本篇文章将从源码的角度去分析 HashMap 的 put,resize,get 流程。如果只想了解这三个方法的原理的小伙伴,可以只看这三个方法末尾的总结部分。
部分变量说明
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
|
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
transient Node<K,V>[] table;
|
初始化流程
构造方法
JDK8 的 HashMap 提供多个构造方法,平时常用的无参构造方法如下,这里只是简单的给装载因子取默认值:
1 2 3
| public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; }
|
构造方法并没有直接创建存储数组,而是在首次调用 put 时才会初始化(table == null 时触发)。
put 流程
入口方法
1 2 3
| public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
|
核心逻辑(putVal)
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
| 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 ((tab = table) == null || (n = tab.length) == 0) { n = (tab = resize()).length; } if ((p = tab[i = (n - 1) & hash]) == null) { tab[i] = newNode(hash, key, value, null); } else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) { e = p; } else if (p instanceof TreeNode) { e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); } else { 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; } } if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) { e.value = value; } afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) { resize(); } afterNodeInsertion(evict); return null; }
|
putVal 流程总结
- 哈希计算:入口方法中,通过
hash(key) 得出hash值。
- 判断链表数组是否被初始化过,如果没有则初始化,默认长度是16。
- 通过hash值定位数组下标:
(n - 1) & hash。
- 处理哈希冲突:
- 链表插入:遍历链表直到找到空节点或相同 key。
- 树化:链表长度 ≥8 时转红黑树(需数组长度 ≥64,否则优先扩容)。
- 扩容检查:元素总数超过阈值
threshold(容量 × 装载因子)时触发 resize()。
扩容(resize)
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 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) { newThr = oldThr << 1; } } else if (oldThr > 0) { newCap = oldThr; } else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { 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; } } } } } return newTab; }
|
resize 流程总结
- 元素数量达到扩容阈值时触发扩容(装载因子 * 数组容量)。
- 新建一个链表数组,新长度和新扩容阈值都是旧链表数组的两倍。
- 把旧表的数据复制到新表中,采用了高低位链表法避免了重新哈希。
- 由于扩容需要把元素重新放入到新表,并且需要遍历一次链表,所以扩容的时间复杂度是O(n)。
get 流程
入口方法
1 2 3 4
| public V get(Object key) { Node<K,V> e; return (e = getNode(key)) == null ? null : e.value; }
|
核心逻辑(getNode)
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
| final Node<K,V> getNode(Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & (hash = hash(key))]) != null) { if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) { return first; } if ((e = first.next) != null) { if (first instanceof TreeNode) { return ((TreeNode<K,V>)first).getTreeNode(hash, key); } do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { return e; } } while ((e = e.next) != null); } } return null; }
|
get流程总结
getNode 的流程比较简单,先是计算出数组的下标,然后通过遍历链表/红黑树比较key值来获取元素。总结如下:
- 哈希定位:
(n - 1) & hash 快速找到数组下标。
- 节点查找:
- 链表遍历时间复杂度O(n)。
- 红黑树查找时间复杂度O(log n)。
总结
| 操作 |
时间复杂度 |
核心逻辑 |
| 初始化 |
O(1) |
延迟初始化,容量为 2 的幂次方 |
| put |
O(1)~O(log n) |
哈希定位 → 链表/树插入 → 扩容检查 |
| get |
O(1)~O(log n) |
哈希定位 → 链表/树遍历 |
| resize |
O(n) |
容量翻倍 → 数据迁移优化 |