1. HashMap的数据结构图
HashMap采⽤Entry数组来存储key-value对,每⼀个键值对组成了⼀个Entry实体,Entry类实际上是⼀个单向的链表结 构,在JDK1.8中,链表⻓度⼤于8的时候,链表会转成红⿊树。
2. 为什么使用链表+数组
数组的长度是限制死的,我们在对key值进行散列取到下标以后,放入到数组中时,难免出现两个key值不同,但是却放入到下标相同的格子中,此时我们就可以使用链表来对其进行链式的存放。
3. LinkedList代替数组结构可以吗?
原则上来说也是可以的。
Entry[] table=new Entry[capacity];
替换为
List<Entry> table=new LinkedList<Entry>();
4. 那既然可以使用进行替换处理,为什么有偏偏使用到数组呢?
数组效率查询最⾼! 在HashMap中,定位节点的位置是利⽤元素的key的哈希值对数组⻓度取模得到。此时,我们已得到节点的位置。 LinkedList
还需要进行遍历,才能查到对于的位置。
即使是用ArrayList,也不好,因为用基本类型数组,扩容机制可以⾃⼰定义,HashMap中数组扩容刚好是2的次幂,在做取模运算的效率⾼。
5. HashMap是如何获取到下表值的
hashMap对存放进来的key值进行了hashcode(),生成了一个值,但是这个值很大,我们不可以直接作为下标, 所以Hashmap对其进行了与操作,对Table的表长度减一再与生成的hash值进行相与操作
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
6. HashMap 中put的过程
- 对key的hashCode()做hash运算,计算index;
- 如果没碰撞直接放到bucket⾥;
- 如果碰撞了,以链表的形式存在buckets后;
- 如果碰撞导致链表过⻓(⼤于等于TREEIFY_THRESHOLD),就把链表转换成红⿊树(JDK1.8中的 改动);
- 如果节点已经存在就替换old value(保证key的唯⼀性)
- 如果bucket满了(超过load factor*current capacity),就要resize
7 . HashMap 中get的过程
- 对key的hashCode()做hash运算,计算index;
- 如果在bucket⾥的第⼀个节点⾥直接命中,则直接返回;
- 如果有冲突,则通过key.equals(k)去查找对应的Entry;
- 若为树,则在树中通过key.equals(k)查找,O(logn);
- 若为链表,则在链表中通过key.equals(k)查找,O(n)。
8. 为什么不一开始就使用红黑树,不是效率很高吗?
因为红⿊树需要进⾏左旋,右旋,变⾊这些操作来保持平衡,⽽单链表不需要。
当元素⼩于8个当时候,此时做查询操作,链表结构已经能保证查询性能。
当元素⼤于8个的时候,此时需要红⿊树来加快查 询速度,但是新增节点的效率变慢了。因此,如果⼀开始就⽤红⿊树结构,元素太少,新增效率⼜⽐较慢,⽆疑这是浪费性能的。
9. 什么时候退化为链表
为6的时候退转为链表。中间有个差值7可以防⽌链表和树之间频繁的转换。
假设⼀下,如果设计成链表个数超过8则链表转 换成树结构,链表个数⼩于8则树结构转换成链表, 如果⼀个HashMap不停的插⼊、删除元素,链表个数在8左右徘徊,就会 频繁的发⽣树转链表、链表转树,效率会很低。
10. 一些特殊的变量名
DEFAULT_INITIAL_CAPACITY
Table数组的初始化长度:1 << 4
2^4=16
(为什么要是 2的n次方?)MAXIMUM_CAPACITY
Table数组的最大长度:1<<30
2^30=1073741824
DEFAULT_LOAD_FACTOR
负载因子:默认值为0.75
。 当元素的总个数>当前数组的长度 * 负载因子。数组会进行扩容,扩容为原来的两倍(todo:为什么是两倍?)TREEIFY_THRESHOLD
链表树化阙值: 默认值为8
。表示在一个node(Table)节点下的值的个数大于8时候,会将链表转换成为红黑树。UNTREEIFY_THRESHOLD
红黑树链化阙值: 默认值为6
。 表示在进行扩容期间,单个Node节点下的红黑树节点的个数小于6时候,会将红黑树转化成为链表。MIN_TREEIFY_CAPACITY = 64
最小树化阈值,当Table所有元素超过改值,才会进行树化(为了防止前期阶段频繁扩容和树化过程冲突)。