Java中HashSet解读以及底层源码
HashSet
Set接口说明
1). Set 接口是 Collection 的子接口, Set 接口的实现类不能存放重复的元素
可以添加null, 且存放的次序是无序的(存放和取出的顺序不一致)
注意: 取出的顺序虽然不是添加的顺序, 但是是固定的
2). 可以利用迭代器遍历, 但是不能使用下标索引遍历
HashSet说明
1). HashSet底层其实是HashMap

2). add() 是HashSet 添加元素的方法, 会返回一个Boolean, 成功后返回true, 失败返回false, 其实失败就是有重复元素
这边重复其实是按照底层分析的
例:
class Cat {
private String name;
public Cat(String name) {
this.name = name;
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
'}';
}
}
public class Main {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add("jack"); // 成功
hashSet.add("jack"); // 失败
hashSet.add(new Cat("tom")); // 成功
hashSet.add(new Cat("tom")); // 成功
hashSet.add(new String("111")); // 成功
hashSet.add(new String("111")); // 失败
System.out.println(hashSet); //[111, Cat{name='tom'}, Cat{name='tom'}, jack]
}
}
这里String 有一个坑, 留待源码解读部分解释
HashSet扩容机制
结论
1). HashSet 底层是 HashMap
2). 添加一个元素时, 先得到hash值, 然后会转成索引值
3). 找到存储数组表Table, 看这个索引位置是否存在元素
4). 如果没有就直接加入, 如果有, 就会调用equals() 比较, 如果相同就会放弃添加, 如果不相同, 就添加到最后
5). 在Java8中, 如果一条链表的元素个数超过默认值(8), 并且Table大小 >= 默认值(64), 就会进行树化
源码解读
代码
public class Main {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add("java");
hashSet.add("php");
hashSet.add("java");
System.out.println(hashSet);
}
}
1). 首先进入构造器, 底层是一个HashMap

2). 随后是add() 方法

这时e = "java"
3). 然后调用map的put方法

这里可以发现传入了两个值, key和Value, 这里key就是当前要加的字符串"java", 而Value就是之前放入的PRESENT,
这里是HashSet 为了使用HashMap 而设置的一个final static类型的Object对象, 用来占位, 没有实际意义
所以之后不管add多少次, Value都不会变
4). 这里首先看hash方法

可以发现这里是先求出来key的hashCode值, 然后异或上他的右移十六位的值, 为的是防止冲突
- 获得hash值之后就会进入
putVal方法, 这个方法较为庞大, 是核心方法

- 首先定义一些辅助变量
table是放Node<>节点的一个数组, 属于HashMap- 如果当前
table为空, 就会执行一个resize()方法, 这也是一个比较庞大的方法, 这里不会过多赘述
大概作用就是扩容到16个空间, 并且里面还会根据加载因子(一般是0.75)来计算一个临界值threshold- 根据传入的key的hash值来计算该key应该存放到
table的哪个位置, 并且将这个位置赋值为p
如果这个值为空, 表示还没有存放过元素, 就要new一个新Node- 如果不为空, 就会进入else中
if : 当前索引位置对应的链表的第一个元素的hash值和传入的hash值一样 && (key一样 || equals()相同), 就将e 置为 p
else if : p是不是一颗红黑树, 如果是红黑树, 就调用putTreeVal进行添加, 这里不再赘述
else : 这个时候就是一个链表, 并且表头和当前添加的对象不同, 这个时候就该遍历链表找有没有相同的对象
如果发现一个一样, 就会将e置为当前重复的对象, break, 否则就会走到最后, 将待添加的对象添加到尾部, e就为null
添加到链表后会判断当前链表的size, 如果大于8, 就会树化afterNodeInsertion是一个空方法, 不需要管, 是为了让子类实现的- 返回null代表成功, 返回的如果是一个对象, 那么就说明这个对象重复
扩容机制补充
1). 第一次添加时, table 扩容到16, 临界值threshold是16 * 加载因子LoadFactor(一般是0.75) = 12
2). 如果table数组 使用到临界值12, 就会扩容到16 * 2 = 32, 新的临界值就是32 * 0.75 = 24, 以此类推
3). 在Java8中, 如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8), 并且table.length >= MIN_TREEIFY_CAPACITY(默认是64), 就会进行树化, 否则仍然采用数组的扩容机制

浙公网安备 33010602011771号