一、HashSet
(1)HashSet底层是HashMap
其实下面完全是因为hashmap的原因导致的
(2)添加一个元素时,先得到hash值–>会转成索引值
(3)找到存储数据表table,看到这个索引位置是否已经存放的有元素
(4)如果没有,直接加入
(5)如果有,调用equals比较,如果相同,就放弃添加,如果不同,则添加到最后
(6)在Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
HashSet的底层结构
HashSet的构造方法和add方法的源码如下
我们发现其实都是调用map的相关方法,add方法的本质会把元素当做map的key,而value是下面的object类型的常量:
由于一切都是跟HashMap挂钩的,那么分析HashMap的底层就尤为重要!
二、HashMap
HashMap的底层结构是:数组+单链表+红黑树
1)HashMap底层维护了Node类型的数组table,默认为null
2)当创建对象时,将加载因子(loadfactor)初始化为0.75.
3)当添加key-val时,通过key的哈希值得到在table的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有完素,继续判断该元素的key和准备加入的key相是否等,如果相等,则直接替换val;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。
4)第1次添加,则需要扩容table容量为16,临界值(threshold)为12(16*0.75)
5)以后再扩容,则需要扩容table容量为原来的2倍(32),临界值为原来的2倍,即24,依次类推.
6)在Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),粗Btable的大小>= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
HashMap的底层结构
其实说白了底层就是维护下面Node类型的数组
构造方法分析
如果是无参构造,会给负载因子loadFactor赋予默认值0.75。注意!table依旧是空数组,在第一次添加元素时,才会扩容到16
如果是有参构造:
关键代码分析
HashMap的底层代码非常的复杂,这里我们只调出重点的地方说一下
分析增加元素的过程:其代码如下
其中hash(key)方法的源码如下图所示,从源码我们可以看出hash值并不是简单的hashCode()
1.其中最核心的代码就是putVal(),该方法是整个HashMap的核心,下面是我对源码的解读注释:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;//定义辅助变量
//table就是HashMap的一个数组,类型是Node[]
//if语句表示如果当前table是null,或>=0
//就是第一次扩容,扩容到16个空间
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//(1)根据key,得到hash 去计算该key应该存放到table表的哪个索引位置
//并把这个位置的对象,赋给p
//(2)判断这个key是否为空
//(2.1)如果key==null,表示还没存放过元素,就创建一个Node(k-v)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//(2.2)如果key!=null,表示该索引处存放过元素
else {
//一个开发技巧提示:在需要局部变量(辅助变量)的时候再创建
Node<K,V> e; K k;//辅助变量
if (p.hash == hash &&//如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样
//并且满足下面的两点
//(1)准备加入的key 和 p指向的Node结点的key是同一个对象
//(2)p指向的Node节点的key的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);
//p为链表的情况
else {
//(1)依次和该链表的的每一个元素比较
for (int binCount = 0; ; ++binCount) {
//都不相同,则加入到该链表的最后
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//注意在把元素添加到链表后,立刻判断该链表是否已经达到8个节点
//如果是的,就调用treeifyBin()对当前这个链表进行树化(转成红黑树)
//注意,在转成红黑树时,treeifyBin()内部会判断tab大小是否小于64,如果是的话,先对tab进行扩容,如果不是才转成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果有相同的情况,就直接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)//如果增添元素后,size达到了临界值(容积*负载因子),就会进行扩容操作
resize();
afterNodeInsertion(evict);
return null;
}
2.扩容代码的解析
触发扩容的情况:
(1)空参new出的hashmap第一次put元素
(2)增加元素后,size达到了临界值(容积*负载因子)
(3)链表长度达到8,且table的长度小于64,会发生扩容。扩容到64就会把链表转成红黑树
挑出核心代码进行分析,下面是第一次put元素时走的代码(因为此时table数组为空嘛)
后面我们会让临界值等于上面的newThr,并让table等于扩容后的newTab
3.树化代码解析
链表长度达到8就会触发下面的函数,如果此时table的长度小于64,则不会发生树化,先对table扩容。直到table大小扩容到64才会把链表转成红黑树。