🏠:博客首页: 进击的波吉
📕:今日分享的文章:Set接口介绍、HashSet源码简要分析
💝:希望自己对源码的解读的可以帮助到大家🎈
🌱:Boji 还在努力学JavaSE ,如有疑问、疏漏之处,请多多指点🙏
☀️:自学成长的路上,感谢大家相伴!No hurry , No Pause !💝
1. Set接口
1.1 Set接口基本介绍
无序
,(添加和取出顺序不一致),没有索引
(Set没有get方法)不允许重复元素
,所以最多包含一个null- 取出的顺序虽不是添加的顺序,但顺序其实是固定的,
Set底层是数组+链表+红黑树
的形式来管理的
1.2 Set接口常用方法
跟List接口一样,Set接口也是Collection的子接口,因此,常用方法和Collection一样,详情可以参见这篇博客:
1.3 Set接口的遍历方式
同Collection遍历方式一样,因为Set接口是Collection接口的子接口(Collection的方法,Set都可以使用)。
- 可以使用迭代器
- 增强for
- 不能使用索引的方式来获取
2.HashSet的底层结构和源码分析
1.HashSet底层是 HashMap
;
2.添加一个元素时,先得到hash值-会转化成- > 索引值
;
3.找到存储表table,看这个索引位置是否已经存放元素 ;
4.如果没有,直接加入;如果有调用equals比较
如果相同就放弃添加,如果不相同,则放到最后 ;
5. 在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认到达8)
,并且table 的大小 >= MIN_TREEIFY_CAPACITY(默认64)
,就会进行树化(红黑树)
。
代码示例:
- 执行HashSet ,初始化HashSet的
底层就是一个HashMap
public HashSet() {
map = new HashMap<>(); //map: null
}
-
执行 add()
, E 是泛型 ,e 是 java 字符串常量
PRESENT 是HashSet里面的一个final类型的的static 对象,对象本身没啥意义,主要起到占位的目的,value里面都是存放的这个
-
执行put方法
key:merry , value:PRESENT(共享)
-
HashSet 计算hashcode方法:若 key == null,必定排在HashSet的第一位,若key != null,
将key对应的hashCode值 逻辑右移16位后的值再进行按位异或 生成新的'hashCode'
,可以理解为 为了避免碰撞,让不同的key 生成不同的hashCode值。
-
执行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; //定义了辅助变量
if ((tab = table) == null || (n = tab.length) == 0) //table 是hashMap 中的一个数组,类型是Node[]
n = (tab = resize()).length; //第一次扩容到 16个空间
if ((p = tab[i = (n - 1) & hash]) == null) // 计算出该 key 的哈希值 去计算该key存放在哈希表的哪个索引位置,并把这个位置对象赋给 p
// 1.如果 p == null ,表示还没有存放元素,就创建一个Node (key, value )
tab[i] = newNode(hash, key, value, null); // 同时也存储了哈希值
else {
Node<K,V> e; K k;
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 {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold) //threshold 大小为 12
resize();
afterNodeInsertion(evict); //hashmap这个方法可以忽略
return null;
}
6.resize()方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // null
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold; // 0
int newCap, newThr = 0;
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
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults 只实现这个
newCap = DEFAULT_INITIAL_CAPACITY; // 初始空间为 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //临界空间为12
}
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"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //创建新的结点 空间大小为16
table = newTab; //table的空间大小也为 16
7.第一个字符串merry ,添加成功,添加 字符串java 方法类似,这里就不再做演示
8.再次添加merry时,在putVal() 方法中,此时的hash值不为 0,
9.判断是否需要树化 以及 HashSet 链表添加的流程
else {
Node<K,V> e; K k;
if (p.hash == hash &&
//如果当前索引位置对应的链表第一个元素和准备添加的key的hash值一样
//或者满足下面两个条件之一:
1.准备加入的key 和 p指向的Node 结点的key 是同一个对象
2.p 指向的Node 结点 的 equals() 和准备加入的key 比较后相同
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 再判断 p 是不是红黑树
// 如果是红黑树,就调用putTreeVal,来进行添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 如果table 对应的索引位置,已经是一条链表,就使用for 循环
(1) 依次和该链表的每一个元素比较后,都不相同,则加入到链表的最后
注意把元素添加到链表后,立即判断,该链表是否已经达到8个结点 ;
就调用treeifyBin()对当前这个链表进行树化(转成红黑树),再进行树化是要进行判断(即table的长度要>= 64)
(2)依次和该链表的每一个元素的比较过程中,如果有相同的情况,就直接break;
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
💝学习总结: 在阅读 HashSet的源码过程,相比较 ArrayList的源码,难度的确大了很多。视频是跟随者韩老师的java课程学习的,收益颇丰,为写搞清debug过程,自己大抵看了三遍吧,才算懂得更清楚一些。HashSet的主要就是底层 hashTable 和 每一个表格 是Node类型 最多可以连接 8个节点,当有链表结点数 大于 等于8 时以及 hashTable 大于等于64时,就会进行红黑树化!哈哈哈,大抵就是这样吧!