有关HashMap一些问题的个人总结

HashMap1.7与1.8的区别

1)1.7底层:数组+链表;1.8:数组+链表+红黑树
2)1.7:头插法;1.8:尾插法

ps:HashMap在jdk1.7中采用头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下可能导致链表成环,在进行下次put操作的时候程序会陷入死循环。而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。

3)1.7:先扩容后插入;1.8:先插入后扩容
4)1.7:计算hash运算多;1.8:计算hash运算少

JDK1.8以后的hashmap为什么在链表长度为8的时候变为红黑树,以及为啥使用红黑树

1)为啥不直接使用红黑树,反而要加上链表了?
因为树节点所占空间差不多是普通节点的两倍,所以只有当节点足够多的时候,才会使用树节点。也就是说,节点少的时候,尽管时间复杂度上,红黑树比链表好一点,但是红黑树所占空间比较大,综合考虑,认为只能在节点太多的时候,红黑树占空间大这一劣势不太明显的时候,才会舍弃链表,使用红黑树。

2)为啥8以后就选择红黑树
链表转红黑树本身就是一种性能开销,选择为8的话应该是避免频繁的转换,在jdk源码中有说明,一个哈希桶上链表长度为8的概率大约为1亿分之8,可见概率很低。这样就避免了频繁地转化

3)既然概率低为啥还要在底层实现链表转红黑树的操作
事实上,链表长度超过 8 就转为红黑树的设计,更多的是为了防止用户自己实现了不好的哈希算法时导致链表过长,从而导致查询效率低,而此时转为红黑树更多的是一种保底策略,用来保证极端情况下查询的效率。

HashMap扩容机制

jdk1.7:
1)创建对象时会初始化一个空数组
2)先扩容后添加元素
3)扩容的条件(满足以下两个)
1、存放新值的时候当前已有元素的个数必须大于等于阈值
2、 存放新值的时候当前存放数据发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值,好处在于可以保证数据结构更简单)

jdk1.8:
1)创建对象时不会初始化hash表,会在第一次进行.put()方法进行初始化
2)先添加元素后扩容++size>threshold
3)扩容的条件(满足其中一个就行):
1、当map实际数量大于threshold容量的阈值时,会进行两倍扩容。
2、当map中数组中某个桶的链表长度大于树形化阈值TREEIFY_THRESHOLD=8时,
并且map元素的数量小于树形化最小容量MIN_TREEIFY_CAPACITY=64时候,容量进行两倍扩容。(其实就是用扩容代替链表转红黑树操作)

HashMap初始容量注意事项:

1)当使用无参构造方法时,默认容量为16
2)当使用有参构造方法时,其容量为大于你传入的数字的最小的2的n次幂(最小1,2的0次幂,传入0容量就是1)

HashMap容量为啥定义为2的n次幂

只要数组大小是2的幂,则 (n-1) & hash 的结果等效于: hash % (n-1);即与运算、求余运算通过这个前提,实现了等效,通过将质数参与到hash运算中可以提高数据插入后的分散性,降低hash冲突发生的概率

HashMap链表转红黑树,红黑树转链表

static final int TREEIFY_THRESHOLD = 8; 链表转红黑树的阈值
static final int UNTREEIFY_THRESHOLD = 6; 红黑树转链表的阈值
static final int MIN_TREEIFY_CAPACITY = 64; 链表转红黑树所需达到的最小容量
当链表需要转红黑树时如果此时map容量没有达到64会用扩容操作来代替转化操作

总结:
1)hashMap并不是在链表元素个数大于8就一定会转换为红黑树,而是先考虑扩容,扩容达到默认限制后才转换
2)hashMap的红黑树不一定小于等于6的时候就会转换为链表,而是只有在resize的时候才会根据 UNTREEIFY_THRESHOLD 进行转换

你可能感兴趣的:(java)