1. 底层数据结构
数组和链表(1.8之后加入红黑树)
2.插入链表的方式
在使用对象的hashcode和链表长度-1取余之后得到下标,对象放入对应的下标位置。如果该位置已经有值,则形成链表排列。
1.7之前插入链表的方式是头插法,但如果在并发情况下可能会造成链表的死循环:
比如 原数组的某条链表是 1->2,那么两个线程同时添加3的时候造成扩容,线程t1扩容数据迁移之后,这三个节点恰巧还是在同一个数组位置上,2->1->3。此时t2也开始数据迁移,处理的第一个节点还是刚加入进来的3,就会把3的next指向1。如此死循环造成。
1.8之后插入链表的方法改成了尾插法,如上面的情况,链表顺序始终不变,因此没有死循环可能。
3.扩容
负载因子0.75,容量若超过当前容量*负载因子,则扩容。
扩容分两步:
- 新建一个两倍于原长度的数组
-
对原数组所有元素进行rehash,重新计算位置。
4.为何要rehash,而不直接复制
长度变了,计算下标的方式和长度有关。
5.为什么1.8之后加入了红黑树
链表在最坏情况的查找效率还是O(n),那么hashmap就失去了意义。红黑树把链表的查询效率提高到了O(log(n))
6.尾插法不会造成死循环,是不是意味着可以在并发中使用
并不是,hashmap的get/set方法并没有加锁,并发使用可能会出现上一秒存进去的数据,下一秒取出来key和value对应不上。
7.初始长度为什么是16,而且自定义最好是2的幂?
计算下标算法是 (len - 1) & hashcode,16-1或者2的幂-1能保证 转化为二进制之后全是1,这样与之后能完全保存hashcode的后几位特征。
附加回答:此外因为hashmap有一个寻址优化的算法,在计算下标i的时候会将 hashcode与自身右移16位的值异或,得到的值可以同时保存hashcode高十六位和低十六位的特征,以此减少hash冲突。
8.为啥重写了equals之后需要重写hashcode()?
避免在get的时候出现值相等,但是却因为hashcode不同而在hashmap中存了两个位置的情况。
9.并发hashmap使用不了,那用啥呢?
hashtable和CurrentHashMap,前者全片synchronized效率极低,后者加入了桶的概念,相当于在hashtable上加了一个维度,每次给一个桶加上锁,提高效率。(粗略分析,后边再博客单讲)