基本使用
1 2 3 4
| Map<String, String> map = new HashMap<>(); map.put("name", "cloudylc"); String name = map.get("name"); System.out.println(name);
|
Map的族谱
classDiagram
class Map {
<<interface>>
}
class SortedMap {
<<interface>>
}
class NavigableMap {
<<interface>>
}
class AbstractMap {
<<abstract>>
}
class Dictionary {
<<abstract>>
}
class HashMap
class LinkedHashMap
class TreeMap
class Hashtable
Map <|.. AbstractMap
AbstractMap <|-- HashMap
HashMap <|-- LinkedHashMap
Map <|-- SortedMap
SortedMap <|-- NavigableMap
NavigableMap <|.. TreeMap
HashMap <|-- TreeMap
Map <|.. Hashtable
Dictionary <|-- Hashtable
底层结构
在jdk1.8中,HashMap的底层结构,主要由 数组+链表/红黑树 构成。
首先HashMap内部维护了一个泛型的Node数组,数组内每一个槽都称之为桶,数组具有以下几个性质:
1
| transient Node<K,V>[] table;
|
当发生哈希冲突时,jdk1.8会先采用链地址法来解决,将冲突的节点通过尾插法加入链表,链表由Node.next 指针实现
1 2 3 4 5 6
| static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; }
|
在当个桶内的链表长度一直增长时,如不干预,对该桶的操作就会退化成O(n),性能会受到影响。
为解决此问题,在一定条件下该桶内链表则会升级成红黑树,树化通过TreeNode对Node的继承使用多态实现
树化的条件如下,两者需要同时满足:
- 单个桶内链表长度 >=
TREEIFY_THRESHOLD(8)
- Node数组长度 >=
MIN_TREEIFY_CAPACITY(64)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| static final class HashMap.TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; boolean red; }
static class LinkedHashMap.Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
|
索引定位的设计解析
在未了解到HashMap源码实现前,仅凭对Hash结构的了解,作者第一印象索引定位算法如下:
1
| int index = key.hashCode() % n;
|
其中 n 就是 Node[] 的长度,通过对 key 的 hash 值对其取余,则能在 n 的范围内,定位到数组索引的位置
但在解析了源码后发现,事情并没有这么简单, HashMap底层的索引定位算法如下:
1 2
| int hash = hash(key); int index = (n - 1) & hash;
|
与上述算法相比,有三处明显的不同:
- 索引定位的计算没有用取余
% 而是用了 &
- 参与索引定位的hash值额外经过hash()扰动函数
位与代替取余
针对第一点可能不太好理解,但是再加上一个前提:Node[] 的长度始终是2的n次幂
在该前提下,对于数学上的 hash % n 效果,使用位移方式 hash & ( n - 1) 可等价实现
原因也很简单:
除了效果等价外,位运算&相较于取余%在性能上会更有优势
- 前者是简单的位运算,一条指令即可,速度极快
- 后者需要用到整除单元,延迟明显更高
扰动函数
为什么要进行二次hash处理,使得hash值得到充分扰动?
因为根据上述的分析可知,对于 (n - 1) & hash,结果就取决于 hash 的低位。
所以 hash 的低位特别重要,假设直接使用 hash = key.hashCode(),如果hash值质量低,存在着较高的哈希碰撞风险。
HashMap 1.8 中的扰动源码如下:
static final int hash(Object key) {1 2 3 4
| static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
|
扰动函数的结果,就是对 key.hashCode() 进行扰动处理,使期更加的散列,以减少哈希碰撞。
并且对于 key = null 的特殊情况,给出一个相同的结果 0 值,使得 key = null 都定位在同一个槽中。
而扰动和核心就是 key.hashCode() ^ (key.hashCode() >>> 16)
即将 hashCode() 的 高16位移动到低位,将高位信息拉到低位,然后与原 hashCode() 进行异或计算。
最终得出一个低位更具随机性的 hashCode,从而使得索引定位更加均匀,哈希碰撞概率更低!
put 流程
简易的源码如下,可以看到,put()底层是调用了一个参数更加详细的putVal()方法
1 2 3 4 5 6 7
| public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { }
|
简易流程
- 计算 key 的扰动后 hash 值
- 表空则初始化
- 索引定位,判断有无发生哈希冲突
- 无冲突,则在槽为上新增节点
- 有冲突,判断节点类型
- 节点是红黑树节点,则添加红黑树节点
- 节点是链表,则遍历尾插,再判断当前链表是否需要树化
- 若存在相同节点,则覆盖值
- 更新计数器
- 检查是否需要扩容
put源码与注释
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
| 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; }
|
resize 流程
简易流程
resize() 最核心的作用就是初始化与扩容。
先计算出新的容量与新的阈值,都按照2倍递增,并使用新容量创建新数组。
针对单个节点,直接使用节点上存的hash与上新容量-1得出新数组的位置。
针对红黑树节点,则进行红黑树的节点拆分。
针对链表节点,则根据 e.hash & oldCap == 0 来拆分高位/低位链表。
- 低位链表,以原位置存入新数组中
- 高位链表,以原位置+oldCap存入新数组中
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 94 95 96 97 98 99 100 101 102 103 104
| 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; }
|
get 流程
简易流程
- 计算出目标的hash值,并定位到到节点
- 检查第首节点,如果与目标值相等则返回
- 存在后续节点,则继续进行检查
- 节点属于红黑树节点,则进行红黑树查找
- 节点属于链表,则逐个遍历对比检查
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 27 28 29 30 31 32 33 34 35
| public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != 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; }
|