HashMap构造方法 HashMap(int initialCapacity, float loadFactor) loadFactor 加载因子
loadFactor 是加载因子,默认是0.75,当 实际节点数 / 总容量 超过 loadFactor 会触发扩容机制
initialCapacity 初始容量 HashMap内部的 Node<K, V> 数组的大小总为2的n次方,这个与求 hash 有关,会在 put() 方法内讨论,因此在构造方法内部,threshold 只能为比 initialCapacity 大的2的n次方
threshold 代表着哈希表进行扩容操作的阈值,当数组的实际容量超过 threshold , 会触发扩容机制,但在构造方法中,threshold 大小为HashMap的初始大小,其值会在第一次 put() 改变
tableSizeFor(int cap) 计算前导零 这个方法会将 initialCapacity 扩大到比 initialCapacity 大的2的n次方
1 2 3 4 5 6 7 8 9 10 11 12 public static int numberOfLeadingZeros (int i) { if (i <= 0 ) return i == 0 ? 32 : 0 ; int n = 31 ; if (i >= 1 << 16 ) { n -= 16 ; i >>>= 16 ; } if (i >= 1 << 8 ) { n -= 8 ; i >>>= 8 ; } if (i >= 1 << 4 ) { n -= 4 ; i >>>= 4 ; } if (i >= 1 << 2 ) { n -= 2 ; i >>>= 2 ; } return n - (i >>> 1 ); }
例如 18 的二进制为 0000 0000 0000 0000 0000 0000 0001 0010 ,其前导零为27
1 2 3 4 static final int tableSizeFor (int cap) { int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1 ); return (n < 0 ) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1 ; }
先把传入的容量 cap 减 1,这是为了处理 cap 本身就是 2 的幂次方的情况。要是 cap 已经是 2 的幂次方,减 1 之后就可以确保最终结果还是 cap 本身。 -1 在的二进制为 1111 1111 1111 1111 1111 1111 1111 1111 ,-1 向右移动 Integer.numberOfLeadingZeros(cap - 1) 位,会出现从左边1开始到末尾全为1的情况,最后返回值再加上1,会变成2的n次方例如:
1 0000 0000 0000 0000 0000 0000 0001 1111 + 1 --> 0000 0000 0000 0000 0000 0000 0010 0000 变成32
HashMap(Map<? extends K, ? extends V> m) HashMap(Map<? extends K, ? extends V> m) 会调用 putMapEntries(Map<? extends K, ? extends V> m, boolean evict) 方法。用 map 数组初始化HashMap,会先计算 map 数组的大小 s ,然后计算hash总容量应该为多少 ==float ft = ((float)s / loadFactor) + 1.0F;== ,再然后判断 ft 是否超出最大容量,如果是则将 ft 赋值为最大容量,最后遍历 map 数组将其存入 hash 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public HashMap (Map<? extends K, ? extends V> m) { this .loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false ); } final void putMapEntries (Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0 ) { if (table == null ) { float ft = ((float )s / loadFactor) + 1.0F ; int t = ((ft < (float )MAXIMUM_CAPACITY) ? (int )ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t); } else { while (s > threshold && table.length < MAXIMUM_CAPACITY) resize(); } for (Entry<? extends K , ? extends V > e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false , evict); } } }
put(K key, V value) hash(Object key) 求hash值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); } putVal(hash(key), key, value, false , evict); final V putVal (int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; 。。。。。。 if ((p = tab[i = (n - 1 ) & hash]) == null ) tab[i] = newNode(hash, key, value, null ); 。。。。。。 }
将h右移16位,目的是h的高位也能参与到后续的桶索引计算中,进而减少哈希冲突的可能性 如果 n 是2的n次方,那么 ==(n - 1) & hash == n & hash== ,相当于求余,提升效率
putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) 插入或更新
在第一次插入数据时,会初始化table数组
1 2 if ((tab = table) == null || (n = tab.length) == 0 ) n = (tab = resize()).length;
然后计算索引并查看索引所在的位置是否有元素
1 2 3 if ((p = tab[i = (n - 1 ) & hash]) == null ) tab[i] = newNode(hash, key, value, null );
情况1: 原位置的key等于插入的key,将原数据 p 赋值给 e
1 2 3 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
情况2: 要插入的位置是红黑树
1 2 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this , tab, hash, key, value);
情况3: 要插入的位置是 链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 for (int binCount = 0 ; ; ++binCount) { if ((e = p.next) == null ) { p.next = newNode(hash, key, value, null ); if (binCount >= TREEIFY_THRESHOLD - 1 ) treeifyBin(tab, hash); break ; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break ; p = e; }
元素e的情况: 元素 e 不为空说明要更新数据,而不是要插入数据
1 2 3 4 5 6 7 if (e != null ) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null ) e.value = value; afterNodeAccess(e); return oldValue; }
最后: 操作数加一,并检查是否需要扩容
1 2 3 4 5 ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null ;
resize() 扩容 因为每次扩容都是翻倍,与原来计算的 (n-1)&hash的结果相比,只是多了一个bit位,所以节点要么就在原来的位置,要么就被分配到”原位置+旧容量 “这个位置 例如我们从16扩展为32时 因此元素在重新计算hash之后,因为n变为2倍,那么n-1的标记范围在高位多1bit(红色),因此新的index就会发生这样的变化: 说明:5是假设计算出来的原来的索引。这样就验证了上述所描述的:扩容之后所以节点要么就在原来的位置,要么就被分配到”原位置+旧容量 “这个位置。
因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就可以了,是0的话索引没变,是1的话索引变成“原索引+oldCap(原位置+旧容量 )”。可以看看下图为16扩充为32的resize示意图:
第一次扩容的代码逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null ) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0 ; newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int )(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node [newCap]; table = newTab; return newTab; }
正常扩容逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 Node<K,V>[] oldTab = table; int oldCap = (oldTab == null ) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0 ; newCap = oldCap << 1 ; newThr = oldThr << 1 ; threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node [newCap]; table = newTab; for (int j = 0 ; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null ) { oldTab[j] = null ; if (e.next == null ) newTab[e.hash & (newCap - 1 )] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this , newTab, j, oldCap); else { Node<K,V> loHead = null , loTail = null ; Node<K,V> hiHead = null , hiTail = null ; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0 ) { if (loTail == null ) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null ) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null ); if (loTail != null ) { loTail.next = null ; newTab[j] = loHead; } if (hiTail != null ) { hiTail.next = null ; newTab[j + oldCap] = hiHead; } } } }