6-HashSet底层结构和源码分析
6-HashSet底层结构和源码分析
介绍汇总:
- HashSet的全面介绍
- HashSet底层机制说明
- HashSet实践练习
1-HashSet的全面介绍
- HashSet 实现了 Set 接口
- HashSet 实际上是 HashMap(源码)
public HashSet() {
map = new HashMap<>();
}
- 可以存放 null 值,但是只能有一个 null
- HashSet 不保证元素是有序的,取决于 hash 后,在确定索引的结果。
- 不能有重复元素、对象。
- 实践练习
HashSet hashSet = new HashSet();
hashSet.add("lucy") ; // 可以加入
hashSet.add("lucy") ; // 不可加入
hashSet.add(new Dog("tom")) ; // 可以加入
hashSet.add(new Dog("tom")) ; // 可以加入
// 经典面试题
hashSet.add(new String("mark")) ; // 可以加入
hashSet.add(new String("mark")) ; // 不可加入
// 虽然创建了两个内容相同、地址不同的字符串对象,
// 但是这两个字符串对象的 hash 值是相同的
// 满足了 hashSet 底层中的 hash 值相同,但是地址不同
// 而还有一个条件就是调用元素的equals方法进行比较
// String 的 equals 方法中比较的时候字符串内容
// 所以会相等,是为相同元素,无法加入 hashSet 中
System.out.println("====HashSet中列表元素有" + hashSet + "====");
// 区分 String 对象以及 hash 值为啥相同
// 不同对象(意味着地址值不同),hash 相同(这是由于字符串为常量,返回的都是常量池中的哈希值,因为都为 tom ,所以 hashCode 计算时为同一个值)
String tom = new String("tom");
System.out.println("tom的哈希值为" + tom.hashCode());
System.out.println(System.identityHashCode(tom));
String tom1 = new String("tom");
System.out.println("tom1的哈希值为" + tom1.hashCode());
System.out.println(System.identityHashCode(tom1));
/*
在Java中,对象存储在堆内存中,但Java的内存模型并不允许直接访问或获取对象的实际内存地址。
然而,如果你需要区分不同的对象实例,可以使用System.identityHashCode(Object x)方法。
这个方法返回的是对象的系统标识哈希码,它在对象的生命周期内是唯一的(除非对象被垃圾回收后,其内存位置被重新分配给另一个对象)。
虽然这个哈希码不是实际的内存地址,但它可以用于区分不同的对象实例。
需要注意的是,hashCode()方法和System.identityHashCode(Object x)方法返回的值是不同的。
hashCode()方法返回的是对象根据自己的哈希码算法计算出的哈希码,而System.identityHashCode(Object x)方法返回的是对象的系统标识哈希码,
它是由JVM在对象的生命周期内分配的,并且不会改变(除非对象被垃圾回收)。
*/
2-HashSet底层机制说明
- 模拟 HashSet 的底层
package set;
import java.util.Arrays;
public class HashSetStructure {
@SuppressWarnings({"all"})
public static void main(String[] args) {
// 模拟一个 HashSet 的底层(HashMap 的底层结构)
// 1. 创建一个数组,数组的类型是 Node[] ,或者该数组称为 表
// 这里表的容量或长度为 16,是因为 HashMap 初始化后第一次添加数据,会将表的容量默认为 16
Node[] table = new Node[16];
System.out.println("====表中元素有" + Arrays.toString(table) + "====");
// 2. 创建数据结点
Node john = new Node("john", null);
// 3. 将数据结点存入表中
table[2] = john ;
Node jack = new Node("jack", null);
// 4. 将数据结点 jack 挂载到 john 之后,形成一个链表
john.next = jack ;
Node rose = new Node("Rose", null);
// 此时索引为 2 的链表中已存在两个数据结点,当该链表达到一定长度,
// 以及表的容量达到一定大小,该链表就会开始树化,也就是形成红黑树
// 这样做的目的是提高数据管理效率
jack.next = rose ;
Node lucy = new Node("lucy", null);
table[3] = lucy ;
// HashSet 的底层或者可以说是 HashMap 的底层结构(因为HashSet 的底层中维护了一个 HashMap )
// 数组 + 链表 + 红黑树
}
}
/**
* 数据结点,存储数据和链接的下一个数据结点,便于形成链表
*/
class Node {
// 存放数据
Object item ;
// 指向下一个结点
Node next ;
public Node(Object item, Node next) {
this.item = item;
this.next = next;
}
@Override
public String toString() {
return "Node{" +
"item=" + item +
", next=" + next +
'}';
}
}
- HashSet 添加元素底层流程结论
- HashSet 底层是 HashMap
- 添加一个元素时,先得到 hash 值(并不是直接通过 hashcode 方法获得的值,有一个专门的算法会将 hashcode 的值进行转化),然后会根据算法将 hash 转成 表 的索引值
- 找到存储数据表 table ,查看这个索引位置是否已经存放数据元素
- 若没有,直接存入
- 若有,调用 equals 比较,若相同,就放弃添加,若不相同,则添加到最后
- 在 Java 8 中,若一条链表的元素个数超过 TREEIFY_THRESHOLD (默认链表树化的门槛或阈值为 8 ),并且 表 table 的容量 => MIN_TREEIFY_CAPACITY(默认表树化的门槛或阈值为 64),就会进行树化(红黑树)
- HashSet 添加元素底层源码流程分析
- HashMap 底层源码分析
// 添加元素的核心方法有 hash、putval、resize、treeifyBin、putTreeVal
// hash
// 根据传入的 Key 对象生成对应的 hash 值
// 若为 null ,则 hash = 0
// 若不为 null ,则先获取 Key 对象的 hashcode 值然后按位异或 hashcode 值的逻辑右移 16 位
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// resize
// 扩容
final Node<K,V>[] resize() {
// 获取当前表
Node<K,V>[] oldTab = table;
// 获取当前表容量
// 若当前表为空,说明当前表可能是刚初始化的表,则表的容量也为 0
// 若当前表不为空,当前表的容量为 oldTab.length
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 获取当前扩容警戒值
int oldThr = threshold;
// 初始化当前表扩容后的新容量、新扩容警戒值
int newCap, newThr = 0;
// 判断当前表的容量,若是满足此种情况,说明已经扩过容了,不是初始化状态了
// 嵌套条件判断
// (1) 当前表达到或超出最大默认容量,仅需改变当前扩容警戒值,然后返回当前表
// (2) 按当前表的容量扩容二倍给新表且小于默认最大容量,同时还满足当前表大于默认初始容量(16),
// 则新扩容警戒值变为原来的 2 倍,等待之后进行扩容即可
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 判断当前扩容警戒值,满足这一条件说明是通过 HashMap(int) 或者 HashMap(int,float) 初始化的 HashMap
// 将原本在初始化的 threshold 的值(这就是指定的初始化容量)赋给 newCap
// 此时,处理完 newCap 等待后续处理 newThr ,在等待扩容就行了
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 此为 HashMap()初始化的 HashSet 进行首次扩容
// 等待扩容即可
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 此判断是为了处理通过 HashMap(int) 或者 HashMap(int,float) 初始化的 HashMap
// 的 newThr ,完毕之后等待扩容即可
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 将扩容后的扩容警戒值赋给实际对象的扩容警戒值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 根据 newCap 进行扩容
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 将扩容后的新表赋给实际对象的表
table = newTab;
// 将原有表的数据全部复制到当前新表(上一步已替换)
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
// putval
// 存储数据
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 判断当前表
// 1. 获取当前表,并确认是否为空,说明当前表可能为刚初始化的 HashMap (HashSet)
// 2. 获取当前表的长度,并确认是否为空,说明当前表可能执行过 clear 方法
// 满足任一条件后,进行容量处理,并获取处理后的表的长度
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 根据 hash 值计算索引值,并获取索引上的数据元素,然后判断是否为空
// 若为空,直接将该数据元素插入该索引
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 若不为空,开始进行数据比对
else {
Node<K,V> e; K k;
// 先与索引上的数据结点进行比对
// 以下是相同的数据元素,会直接赋给 e ,然后返回旧值,代表元素重复
// 从 hash 和 key 或者 equals(可自定义) 来进行判断
// 相同元素的 hash 值是相同的,所以这个要注意有个哈希值控制方法
// 不同对象的哈希值一定是不同的,但 String 对象除外,之前介绍过
// 而且可以通过重写 equals 和 hashCode 来达到对于同一类的对象如何
// 为相同的可以进行控制
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 树结点的实例
// 是的话进行树结点的元素比对和存储
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 链表
else {
// binCount 是统计链表的元素个数,不包括索引上的数据结点
for (int binCount = 0; ; ++binCount) {
// 查看 p 是否有下一个结点
// 若为 null 的话,直接插入元素即可
// 然后再来判断链表元素个数是否达到树化条件
// 结束循环,之后处理 e
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// p 的下一个结点不为 null
// 进行元素比对,和之前的方式一样
// 元素一致的话就结束循环,之后处理 e
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 判断 e 是否存在值
// 若有的话,则说明有重复元素
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 每一次修改都会统计
++modCount;
// 修改成功后就将大小 + 1
// 然后查看大小是否达到警戒扩容值
// 这个警戒扩容值是针对存储了多少元素的,不是仅对于表中存储的
// 数据个数进行是否扩容的,还包括其链表或者红黑树的数据元素的
if (++size > threshold)
resize();
afterNodeInsertion(evict);
// 返回 null 意味着成功插入
return null;
}
// treeifyBin 树化(红黑树)
// 树化需要满足链表个数(8)和表的容量(>= 64)
// 当满足链表个数的话,表容量不满足,就先进行表容量的扩容
// 还有一个重要点就是当红黑树的个数小于等于 6 的话,红黑树又会转成链表,此过程称为剪枝
// 关于剪枝仅是提一下
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
注:上方代码仅是解释每行代码的大致作用,需要 debug 进行梳理流程。
- HashSet 的扩容注意事项
HashSet hashSet = new HashSet();
// HashSet 底层是 HashMap ,使用不同的数字(hash 一般也不相同)
// 所以存储的位置都在内部数组表上,仅模拟内部数组表的扩容
/*for (int i = 1 ; i <= 100 ; i ++ ) {
hashSet.add(i) ;
}*/
// 使用同一类,并重写 hashCode 方法,使其生成固定的 hash 值
// 这样就让所有数据放置在同一索引上,仅模拟链表树化
for (int i = 1; i <= 12 ; i ++) {
hashSet.add(new Pet(i)) ;
}
// 当链表中个数达到 8 (不包括头结点),而此时内部数组表的容量并未达到默认树化的容量 64,需要先进行给内部数组表扩容一次为 32
// 然后在继续给链表添加元素,又会触发树化,但是内部数组表并未达到要求,只能继续扩容为 64
// 再次添加元素,又会触发树化,此时内部数组表达到要求,会将该链表进行树化,由 Node 结点变换为 TreeNode 结点
// 还有个注意点就是在扩容过程中,链表的头结点的索引又发生了改变
// 注意:给 HashSet 中添加元素时,无论会被添加到哪一个位置(索引上、链表上、红黑树上),在 putVal 中在结尾进行元素个数的增加,
// 然后与扩容警戒值进行比较是否需要扩容(扩内部数组表)
// 虽然超过扩容警戒值会进行内部数组表表的扩容,但是扩容警戒值是针对所有元素,包括索引上、链表上、红黑树上的
3-HashSet实践练习
- 实践练习一
package set;
import java.util.HashSet;
import java.util.Objects;
public class HashSetExercise {
@SuppressWarnings({"all"})
public static void main(String[] args) {
/**
* 定义一个 Employee 类,该类包含:private 成员属性 name、age 要求:
* 1. 创建 3 个 Employee 对象放入 HashSet 中
* 2. 当 name 和 age 的值相同时,认为是相同员工,不能添加到 HashSet 集合中
*
* 解题思路:
* 当 name 和 age 的值相同时,认为是相同员工,就是在 HashSet 为相同的元素
* 而 HashSet (底层 HashMap)中处理是否为相同元素的条件为 p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
* 首先,hash 相同,但不同对象的 hash 不相同,这是调用的 Object 的 hashCode 生成的,可以通过重写父类方法实现,来控制属性成员生成 hash 值,
* 达到不同对象但同一属性成员的值就可以生成相同的 hash 值,这样就满足第一条件了 p.hash == hash
* 对于第二条件,不同对象肯定不满足 (k = p.key) == key
* 只有满足 key != null && key.equals(k) 才能达到题目的要求,所以要重写 equals 方法,当同一类不同对象时,通过比较属性成员的值来确保是否相同
* 然后决定是重复已存在,还是插入
*/
HashSet hashSet = new HashSet();
hashSet.add(new Employee("tom",18)) ;// 可以加入
hashSet.add(new Employee("tom",28)) ;// 可以加入,同名不同龄
hashSet.add(new Employee("tom",18)) ;// 不可以加入,同名同龄,与第一次加入的 hash 值相同
System.out.println("====HashSet元素有" + hashSet + "====");
}
}
class Employee {
private String name ;
private int age ;
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return age == employee.age && Objects.equals(name, employee.name);
}
@Override
public int hashCode() {
// 根据各类对象进行 hash 的生成
// 这种就确保同一类的不同对象的属性成员来控制这两个是不是相同元素了
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
- 实践练习二
package set;
import java.util.HashSet;
import java.util.Objects;
public class HashSetExercise01 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
/**
* 定义一个 Employee01 类,该类包含:private 成员属性 name、salary、birthday(MyDate类型,成员属性包括 year、month、day) 要求:
* 1. 创建 3 个 Employee 对象放入 HashSet 中
* 2. 当 name 和 birthday 的值相同时,认为是相同员工,不能添加到 HashSet 集合中
* 解题思路:
* 当出现同一类的属性成员相同时为相同元素,则我们需要将对象的 hashCode 进行重写,并以属性成员的值作为生成 hash 值,调用 Objects 的 hash
* 方法进行根据传入的参数来生成 hash 值,这样就确保不同对象但有相等的属性值的 hash 值是相同的,这样满足了 HashSet 排除重复元素的第一条件
* hash 相同
* 然后的话,因为是不同对象,所以 HashSet 排除重复元素的另一条件的其中一个条件肯定不满足,对象是否相同
* 那么就必须满足最后一个条件,通过 equals 比较确定是否相同,要是不进行重写的话,肯定不相同(比较的也是对象)
* 所以重写 equals 方法,比较的是它们的对应的属性值
*
* 还有要注意的点就是
* 当属性成员中有自定义类的对象,就得需要改自定义类也得进行上述方法的重写 hashCode 和 equals
* 这个过程比较麻烦,通过 debug 下方代码,看到其中过程就明白其中为啥也要继续重写了,其实和上方的解题思路一样
*/
HashSet hashSet = new HashSet();
hashSet.add(new Employee01("tom",8000,new MyDate(2002,12,8)));
hashSet.add(new Employee01("jack",6000,new MyDate(2002,12,8)));
hashSet.add(new Employee01("tom",8000,new MyDate(2002,12,8)));
System.out.println("====HashSet元素有" + hashSet + "====");
}
}
class Employee01 {
private String name ;
private int salary ;
private MyDate birthday ;
public Employee01(String name, int salary, MyDate birthday) {
this.name = name;
this.salary = salary;
this.birthday = birthday;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee01 that = (Employee01) o;
return Objects.equals(name, that.name) && Objects.equals(birthday, that.birthday);
}
@Override
public int hashCode() {
return Objects.hash(name, birthday);
}
@Override
public String toString() {
return "Employee01{" +
"name='" + name + '\'' +
", salary=" + salary +
", birthday=" + birthday +
'}';
}
}
class MyDate {
private int year ;
private int month ;
private int day ;
public MyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyDate myDate = (MyDate) o;
return year == myDate.year && month == myDate.month && day == myDate.day;
}
@Override
public int hashCode() {
return Objects.hash(year, month, day);
}
@Override
public String toString() {
return "MyDate{" +
"year=" + year +
", month=" + month +
", day=" + day +
'}';
}
}
注:主要明白 HashSet (HashMap)的排除相同元素的条件。