文章目录:博客首页: 进击的波吉
:今日分享的文章: Set接口介绍、HashSet源码简要分析
:希望自己对源码的解读的可以帮助到大家
:Boji 还在努力学JavaSE ,如有疑问、疏漏之处,请多多指点
☀️:自学成长的路上,感谢大家相伴!No hurry , No Pause !
1. Set接口
1.1 Set接口基本介绍1.2 Set接口常用方法1.3 Set接口的遍历方式 2.HashSet的底层结构和源码分析
1. Set接口 1.1 Set接口基本介绍- 无序,(添加和取出顺序不一致),没有索引(Set没有get方法)不允许重复元素,所以最多包含一个null取出的顺序虽不是添加的顺序,但顺序其实是固定的, Set底层是数组+链表+红黑树的形式来管理的
跟List接口一样,Set接口也是Collection的子接口,因此,常用方法和Collection一样,详情可以参见这篇博客:
1.3 Set接口的遍历方式Java集合Collection常用方法详解
同Collection遍历方式一样,因为Set接口是Collection接口的子接口(Collection的方法,Set都可以使用)。
- 可以使用迭代器增强for不能使用索引的方式来获取
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[] tab; Node 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 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)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[] resize() { Node [] 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 [] newTab = (Node [])new Node[newCap]; //创建新的结点 空间大小为16 table = newTab; //table的空间大小也为 16
7.第一个字符串merry ,添加成功,添加 字符串java 方法类似,这里就不再做演示
8.再次添加merry时,在putVal() 方法中,此时的hash值不为 0,
9.判断是否需要树化 以及 HashSet 链表添加的流程
else {
Node 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)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时,就会进行红黑树化!哈哈哈,大抵就是这样吧!



