数据结构:数组+链表。Node<K,V>[] table。(源码参考put()方法),允许null键/值。
数组+链表有什么优点:
- 若是数组,数组缺点:需要连续内存空间;删除需要移动其他元素位置
- 若是链表,链表缺点:查找复杂度高。
- 时间复杂度:O(1)。常量时间(假设能正确分散)。
一、如何确定key的数组索引位置,为什么?
1、得到混合key.hashCode()
高低位的hash值。
1 | static final int hash(Object key) { |
2、在数组table(桶)中的索引下标为:index =hash & (n-1)
,n为桶长度,n为2的幂。
n为2的幂,n-1
用二进制表示,低位以连续1表示(如00001111),hash & (n-1)
结果范围为 [0,1],取决于hash的低位。
当hash
与(n-1)
做位与&
运算时,hash只会有低位参与运算(取决于n-1尾端1的位数量),这也是计算hash时key的hashCode的低16位和高16位做XOR运算的原因,能增加低位的随机性。
用位与&运算,比传统的取模%操作效率更高;索引值不会超过数组长度;
二、为什么容量capacity必须为2的幂?
体现在根据key分配到哪个桶中。跟计算索引位置index = hash & (n-1)
相关。
当n为2的幂 ,hash&(n-1)
结果范围为 [0,1],取决于hash的低位。保证位运算和取余的结果相等,但效率更高。
能减少哈希碰撞,避免不同hash值被分配到同一位置,在计算key的哈希值,hash = key.hashCode()) ^ (h >>> 16
,高16位与低16位异或运算,减少冲突。
三、默认容量多少?若指定时不为2的幂呢?
默认初始容量:16(1<<4),负载因子:0.75f。
若指定initialCapacity
时,不为2^n,hashMap的tableSizeFor()
处理后能保证容量总为2的幂。
1 | static final int MAXIMUM_CAPACITY = 1 << 30; |
四、put()源码
put方法:
1 | public V put(K key, V value) { |
1 | //static final int TREEIFY_THRESHOLD = 8; |
五、hashMap什么时候扩容?
1、put方法步骤1,如果初始化后第一次添加元素,进行resize。hashMap初始化时,并没有对table进行赋值。resize()方法中才有Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
2、put方法步骤4,加入新key/value后,若hashMap中key-value数量大于阈值,即hashMap.size > threshold
时,进行扩容。
1 | /** |
3、put方法步骤3.3.1,treeifyBin()转成红黑树中
1 | //MIN_TREEIFY_CAPACITY = 64 |
六、关于负载因子及阈值threshold?
负载因子loadFactor的默认值是0.75。
阈值threshold取决于loadFactor。实例化hashMap时,若指定HashMap.table的长度(initialCapacity),此时将tableSizeFor(initialCapacity)
赋值给threshold(在resize时threshold用于新table长度,threshold会重新计算得出)。
在扩容resize()方法中,不管table是否被创建,threshold=oldCap*loadFactor。table长度若未指定,默认为16。loadFactor若未指定,默认为0.75f。
threshold针对的是hashMap中key-value的数量,HashMap.size,并非table.size。
七、扩容resize()源码
扩容核心 (e.hash & oldCap),=0时索引没变,!=0时原索引+oldCap
1 | //int threshold; |
八、扩容时HashMap如何确定index?
JDK7中是每次都int i = indexFor(e.hash, newCapacity)
重新计算。
JDK8进行了优化,不需要每次都重新计算。若原数组某索引处只有一个节点时,直接hash & (newCap - 1)
计算新索引。
若多于一个节点(即原索引处发生哈希冲突),新索引位置hash & (newCap - 1)
的结果只会有两种。通过节点的hash值与原数组长度位与运算的结果来判断,e.hash & oldCap
,=0时索引没变,否则原索引+oldCap。