|
1 | 1 | # NOTE |
| 2 | +HashMap |
| 3 | + 存储结构 |
| 4 | + |
| 5 | + 内部包含了一个 Entry 类型的数组 table。 |
| 6 | + |
| 7 | + transient Entry[] table; |
| 8 | + Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值和散列桶取模运算结果相同的 Entry。 |
| 9 | + |
| 10 | + |
| 11 | + |
| 12 | + static class Entry<K,V> implements Map.Entry<K,V> { |
| 13 | + final K key; |
| 14 | + V value; |
| 15 | + Entry<K,V> next; |
| 16 | + int hash; |
| 17 | + |
| 18 | + Entry(int h, K k, V v, Entry<K,V> n) { |
| 19 | + value = v; |
| 20 | + next = n; |
| 21 | + key = k; |
| 22 | + hash = h; |
| 23 | + } |
| 24 | + |
| 25 | + public final K getKey() { |
| 26 | + return key; |
| 27 | + } |
| 28 | + |
| 29 | + public final V getValue() { |
| 30 | + return value; |
| 31 | + } |
| 32 | + |
| 33 | + public final V setValue(V newValue) { |
| 34 | + V oldValue = value; |
| 35 | + value = newValue; |
| 36 | + return oldValue; |
| 37 | + } |
| 38 | + |
| 39 | + public final boolean equals(Object o) { |
| 40 | + if (!(o instanceof Map.Entry)) |
| 41 | + return false; |
| 42 | + Map.Entry e = (Map.Entry)o; |
| 43 | + Object k1 = getKey(); |
| 44 | + Object k2 = e.getKey(); |
| 45 | + if (k1 == k2 || (k1 != null && k1.equals(k2))) { |
| 46 | + Object v1 = getValue(); |
| 47 | + Object v2 = e.getValue(); |
| 48 | + if (v1 == v2 || (v1 != null && v1.equals(v2))) |
| 49 | + return true; |
| 50 | + } |
| 51 | + return false; |
| 52 | + } |
| 53 | + |
| 54 | + public final int hashCode() { |
| 55 | + return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); |
| 56 | + } |
| 57 | + |
| 58 | + public final String toString() { |
| 59 | + return getKey() + "=" + getValue(); |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + |
| 64 | + put操作 |
| 65 | + |
| 66 | + public V put(K key, V value) { |
| 67 | + if (table == EMPTY_TABLE) { |
| 68 | + inflateTable(threshold); |
| 69 | + } |
| 70 | + // 键为 null 单独处理 |
| 71 | + if (key == null) |
| 72 | + return putForNullKey(value); |
| 73 | + int hash = hash(key); |
| 74 | + // 确定桶下标 |
| 75 | + int i = indexFor(hash, table.length); |
| 76 | + // 先找出是否已经存在键为 key 的键值对,如果存在的话就更新这个键值对的值为 value |
| 77 | + for (Entry<K,V> e = table[i]; e != null; e = e.next) { |
| 78 | + Object k; |
| 79 | + if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { |
| 80 | + V oldValue = e.value; |
| 81 | + e.value = value; |
| 82 | + e.recordAccess(this); |
| 83 | + return oldValue; |
| 84 | + } |
| 85 | + } |
| 86 | + modCount++; |
| 87 | + // 插入新键值对 |
| 88 | + addEntry(hash, key, value, i); |
| 89 | + return null; |
| 90 | + } |
| 91 | + |
| 92 | + HashMap 允许插入键为 null 的键值对。但是因为无法调用 null 的 hashCode() 方法,也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。 |
2 | 93 |
|
3 | | - |
| 94 | + private V putForNullKey(V value) { |
| 95 | + for (Entry<K,V> e = table[0]; e != null; e = e.next) { |
| 96 | + if (e.key == null) { |
| 97 | + V oldValue = e.value; |
| 98 | + e.value = value; |
| 99 | + e.recordAccess(this); |
| 100 | + return oldValue; |
| 101 | + } |
| 102 | + } |
| 103 | + modCount++; |
| 104 | + addEntry(0, null, value, 0); |
| 105 | + return null; |
| 106 | + } |
| 107 | + |
| 108 | + 使用链表的头插法,也就是新的键值对插在链表的头部,而不是链表的尾部。 |
| 109 | + |
| 110 | + void addEntry(int hash, K key, V value, int bucketIndex) { |
| 111 | + if ((size >= threshold) && (null != table[bucketIndex])) { |
| 112 | + resize(2 * table.length); |
| 113 | + hash = (null != key) ? hash(key) : 0; |
| 114 | + bucketIndex = indexFor(hash, table.length); |
| 115 | + } |
| 116 | + |
| 117 | + createEntry(hash, key, value, bucketIndex); |
| 118 | + } |
| 119 | + |
| 120 | + void createEntry(int hash, K key, V value, int bucketIndex) { |
| 121 | + Entry<K,V> e = table[bucketIndex]; |
| 122 | + // 头插法,链表头部指向新的键值对 |
| 123 | + table[bucketIndex] = new Entry<>(hash, key, value, e); |
| 124 | + size++; |
| 125 | + } |
| 126 | + |
| 127 | + |
| 128 | + Entry(int h, K k, V v, Entry<K,V> n) { |
| 129 | + value = v; |
| 130 | + next = n; |
| 131 | + key = k; |
| 132 | + hash = h; |
| 133 | + } |
| 134 | + |
| 135 | + 确定桶下标 |
4 | 136 |
|
| 137 | + int hash = hash(key); |
| 138 | + int i = indexFor(hash, table.length); |
| 139 | + |
| 140 | + 计算 hash 值 |
| 141 | + |
| 142 | + final int hash(Object k) { |
| 143 | + int h = hashSeed; |
| 144 | + if (0 != h && k instanceof String) { |
| 145 | + return sun.misc.Hashing.stringHash32((String) k); |
| 146 | + } |
| 147 | + |
| 148 | + h ^= k.hashCode(); |
| 149 | + |
| 150 | + // This function ensures that hashCodes that differ only by |
| 151 | + // constant multiples at each bit position have a bounded |
| 152 | + // number of collisions (approximately 8 at default load factor). |
| 153 | + h ^= (h >>> 20) ^ (h >>> 12); |
| 154 | + return h ^ (h >>> 7) ^ (h >>> 4); |
| 155 | + } |
| 156 | + |
| 157 | + public final int hashCode() { |
| 158 | + return Objects.hashCode(key) ^ Objects.hashCode(value); |
| 159 | + } |
| 160 | + |
| 161 | + 扩容-基本原理 |
| 162 | + 设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的复杂度为 O(N/M)。 |
| 163 | + |
| 164 | + 为了让查找的成本降低,应该尽可能使得 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。 |
| 165 | + |
| 166 | + 和扩容相关的参数主要有:capacity、size、threshold 和 load_factor。 |
| 167 | + |
| 168 | + 参数 含义 |
| 169 | + capacity table 的容量大小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次方。 |
| 170 | + size 键值对数量。 |
| 171 | + threshold size 的临界值,当 size 大于等于 threshold 就必须进行扩容操作。 |
| 172 | + loadFactor 装载因子,table 能够使用的比例,threshold = (int)(newCapacity * loadFactor)。 |
| 173 | + |
| 174 | + static final int DEFAULT_INITIAL_CAPACITY = 16; |
| 175 | + |
| 176 | + static final int MAXIMUM_CAPACITY = 1 << 30; |
| 177 | + |
| 178 | + static final float DEFAULT_LOAD_FACTOR = 0.75f; |
| 179 | + |
| 180 | + transient Entry[] table; |
| 181 | + |
| 182 | + transient int size; |
| 183 | + |
| 184 | + int threshold; |
| 185 | + |
| 186 | + final float loadFactor; |
| 187 | + |
| 188 | + transient int modCount; |
| 189 | + 从下面的添加元素代码中可以看出,当需要扩容时,令 capacity 为原来的两倍。 |
| 190 | + |
| 191 | + void addEntry(int hash, K key, V value, int bucketIndex) { |
| 192 | + Entry<K,V> e = table[bucketIndex]; |
| 193 | + table[bucketIndex] = new Entry<>(hash, key, value, e); |
| 194 | + if (size++ >= threshold) |
| 195 | + resize(2 * table.length); |
| 196 | + } |
| 197 | + 扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中,因此这一步是很费时的。 |
| 198 | + |
| 199 | + void resize(int newCapacity) { |
| 200 | + Entry[] oldTable = table; |
| 201 | + int oldCapacity = oldTable.length; |
| 202 | + if (oldCapacity == MAXIMUM_CAPACITY) { |
| 203 | + threshold = Integer.MAX_VALUE; |
| 204 | + return; |
| 205 | + } |
| 206 | + Entry[] newTable = new Entry[newCapacity]; |
| 207 | + transfer(newTable); |
| 208 | + table = newTable; |
| 209 | + threshold = (int)(newCapacity * loadFactor); |
| 210 | + } |
| 211 | + |
| 212 | + void transfer(Entry[] newTable) { |
| 213 | + Entry[] src = table; |
| 214 | + int newCapacity = newTable.length; |
| 215 | + for (int j = 0; j < src.length; j++) { |
| 216 | + Entry<K,V> e = src[j]; |
| 217 | + if (e != null) { |
| 218 | + src[j] = null; |
| 219 | + do { |
| 220 | + Entry<K,V> next = e.next; |
| 221 | + int i = indexFor(e.hash, newCapacity); |
| 222 | + e.next = newTable[i]; |
| 223 | + newTable[i] = e; |
| 224 | + e = next; |
| 225 | + } while (e != null); |
| 226 | + } |
| 227 | + } |
| 228 | + } |
| 229 | + |
| 230 | + |
| 231 | + 扩容-重新计算桶下标 |
| 232 | + 在进行扩容时,需要把键值对重新计算桶下标,从而放到对应的桶上。在前面提到,HashMap 使用 hash%capacity 来确定桶下标。HashMap capacity 为 2 的 n 次方这一特点能够极大降低重新计算桶下标操作的复杂度。 |
| 233 | + |
| 234 | + 假设原数组长度 capacity 为 16,扩容之后 new capacity 为 32: |
| 235 | + git |
| 236 | + capacity : 00010000 |
| 237 | + new capacity : 00100000 |
| 238 | + |
| 239 | + 对于一个 Key,它的哈希值 hash 在第 5 位: |
| 240 | + |
| 241 | + 为 0,那么 hash%00010000 = hash%00100000,桶位置和原来一致; |
| 242 | + 为 1,hash%00010000 = hash%00100000 + 16,桶位置是原位置 + 16 |
| 243 | + |
| 244 | + |
0 commit comments