HashMap 是日常工作开发中经常使用到的一个集合类,并且 hash 这一类数据结构有着类似的实现,比如 redis 中的 hash 结构与 HashMap 就有着惊人的类似。因此读完 HashMap 会有一种举一反三的感觉,非常值得学习。
继承关系简要图
HashMap类前注释(搓翻译)
挑重点看,挑重点翻译~
一种基于散列表的Map
接口实现。允许null
值与null
键。HashMap
与HashTable
大致相同,区别在于前者是非同步且允许null
。不保证顺序,且顺序可能会变。
如果hash函数足够好,这种实现中的基础操作(如get
、put
)只需常量时间即可。选择初始容量与加载因子非常重要,如果你非常在意Iterator
的表现。
一个HashMap
实例拥有两个影响它的性能的因素:初始容量 和加载因子 。
通常来说,默认的加载因子0.75可以在时间消耗和空间消耗之间取得一个较好的平衡。过高,会减少空间消耗但会增加查看消耗(表现在HashMap
中的大部分操作,包括get
和put
)。
当设置它的初始容量时,为了减少rehash的次数,所预期的元素个数以及加载因子应当被考虑到。如果初始容量 比元素的个数 除以加载因子 的结果 要大,那么将不会发生rehash操作。
如果要存很多元素,给一个充分大的容量给它,将会比“给个小容量然后让其自动增长容量”这种方式更加高效。
如果使用了过多的经过hashCode()
处理后得到相同值的键,无论在任何哈希表中,这都会表现得更慢。为了改善这种影响,当键是Comparable
是,将对他们进行比较。
什么是对Map
的结构性修改?添加或删除某个键值对,修改不是。
需要注意,此类不是线程同步的。
成员变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 ;
如果在某个链表上节点个数达到 8
,会尝试将链表结构转换成红黑树结构,最终是否转换成红黑树,还得看 tab.length
是否达到 MIN_TREEIFY_CAPACITY
。
1 2 transient Node<K,V>[] table;
链表节点 先看链表节点的数据结构。这是一个单链表,其中包括了多项信息,诸如键、值、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 30 31 32 33 34 35 36 37 static class Node <K,V> implements Map .Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { } public final K getKey () { return key; } public final V getValue () { return value; } public final String toString () { return key + "=" + value; } public final int hashCode () { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue (V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals (Object o) { if (o == this ) return true ; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true ; } return false ; } }
上述代码中,使用的Objects
的相应方法如下:
1 2 3 4 5 6 public static int hashCode (Object o) { return o != null ? o.hashCode() : 0 ; } public static boolean equals (Object a, Object b) { return (a == b) || (a != null && a.equals(b)); }
构造函数 共3个。分别用于指定相应的加载因子 与起始容量。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public HashMap (int initialCapacity, float loadFactor) { if (initialCapacity < 0 ) throw new IllegalArgumentException ("..." ); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException ("" ); this .loadFactor = loadFactor; this .threshold = tableSizeFor(initialCapacity); } public HashMap (int initialCapacity) { this (initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap () { this .loadFactor = DEFAULT_LOAD_FACTOR; }
大致总结如下:
入参异常判断
最大值判断。如果超出了MAXIMUM_CAPACITY
,那么将起始容量置为MAXIMUM_CAPACITY
。
正常情况处理。通过 tableSizeFor()
为 initialCapacity
计算一个threshold
,这个值是下次resize()
时,需要扩展到的容量。
tableSizeFor
其计算方式如下:
1 2 3 4 5 6 7 8 9 10 static final int tableSizeFor (int cap) { int n = cap - 1 ; n |= n >>> 1 ; n |= n >>> 2 ; n |= n >>> 4 ; n |= n >>> 8 ; n |= n >>> 16 ; return (n < 0 ) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1 ; }
此处位运算略有抽象,通过代入数值进行手动计算、对比就知道它到底在执行什么操作。
通过无符号右移 与相或 ,可以让原来数二进制的最高位 到最低位 ,全部变成 1 ,也就是大于原数的2^n-1 ,后面加1 ,达到2^n 。
假设传入的 initialCapacity
为 9
,二进制为 1001
,减 1
后为 1000
。如下:
1 2 3 4 5 6 7 8 9 1000 >>> 1 = 0100 1000 | 0100 = 1100 1100 >>> 2 = 0011 1100 | 0011 = 1111 1111 >>> 4 = 0000 1111 | 0000 = 1111 …
在构造函数里面没有对本实例中的容量做任何修改,那当我们初始化一个 HashMap
之后,其中的 capacity
是多少呢?
初始化时,table
为空,所以 capacity
的来源只有 threshold
和 DEFAULT_INITIAL_CAPACITY
(16),分别对应有传 initialCapacity
和未传 initialCapacity
的情况。
1 2 3 4 5 final int capacity () { return (table != null ) ? table.length : (threshold > 0 ) ? threshold : DEFAULT_INITIAL_CAPACITY; }
增加 - put() 从put()
开始,假设我们map.put("first", 1);
。我们将计算出key
的hash值,并跳转到putVal
中。
1 2 3 public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); }
其中传入了两个boolean
变量:
onlyIfAbsent
,为 true
则已存在时覆盖改值
evict
,为构建模式(creation mode)
hash(key)
干了什么?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
键为时就为0,否则先得到键的hashCode()
,无符号右移16位 后,再与原数异或 ,得到键的hash。
接下来进入 putVal
的流程,主要分成 3 块逻辑:
初始化:第一次插入。
数组上没有节点。直接将数组中相应位置,修改成该节点。
数组上存在节点。
如果数组上的第一个 Node
与所 put 的Node
相同,即已存在(判断条件:先判 hash,再判 equals),则视情况(onlyIfAbsent
)来决定是否替换 value。
为树节点,则将节点添加到红黑树。
遍历 Node
链表
找到相同的 Node
,视情况替换。
到达链表末端,添加新节点到链表末端。
如果链表上的数量大于等于 TREEIFY_THRESHOLD
tab.length
小于 MIN_TREEIFY_CAPACITY
时进行 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 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()
。主要分成两部分
计算新的容量 与临界值
将原数组中的节点 按照相应的规律分配到新数组 中
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 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; }
确定新的容量及临界容量 对这一部分代码的总体理解如下:
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 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; Node<K,V>[] newTab = (Node<K,V>[])new Node [newCap]; table = newTab;
接下来对每段代码进行分析,总共分成3个片段,分别对应上述代码中的3中情况。
①Map不为空,里面存在键值对 代码片段:
1 2 3 4 5 6 7 8 9 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 ; }
有一个问题,如下:
为什么要做oldCap >= MAXIMUM_CAPACITY
判断
首先MAXIMUM_CAPACITY
的赋值为1 << 30
,即0x4000 0000
。然而Integer.MAX_VALUE = 0x7fff ffff
,也就是说MAXIMUM_CAPACITY << 1
会变成0x8000 0000
,也就是溢出。
为了模拟一次这种情况,特意编写了下面的代码,运行下面代码的时候,JVM默认的堆大小不够,设了一个最大堆大小为10GB,参数为:-Xmx10240m
。
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 public static void main (String[] args) throws Exception { disableWarning(); showHashMapInfo(0x1fffffff ); System.out.println("------------------------------------" ); showHashMapInfo(0x3fffffff ); } private static void showHashMapInfo (int capacity) throws Exception { System.out.printf("initialCapacity is : %x\n" , capacity); HashMap<String, String> hashMap = new HashMap <>(capacity); Class<?> hashMapClass = hashMap.getClass(); Method resizeMethod = hashMapClass.getDeclaredMethod("resize" ); Method capacityMethod = hashMapClass.getDeclaredMethod("capacity" ); Field thresholdField = hashMapClass.getDeclaredField("threshold" ); resizeMethod.setAccessible(true ); capacityMethod.setAccessible(true ); thresholdField.setAccessible(true ); System.out.printf("After hashMap created, capacity is : %x, threshold is : %x\n" , capacityMethod.invoke(hashMap), thresholdField.get(hashMap)); hashMap.put("" , "" ); System.out.printf("After resize(initialization), capacity is : %x, threshold is : %x\n" , capacityMethod.invoke(hashMap), thresholdField.get(hashMap)); resizeMethod.invoke(hashMap); System.out.printf("After resize(double size), capacity is : %x, threshold is : %x\n" , capacityMethod.invoke(hashMap), thresholdField.get(hashMap)); } private static void disableWarning () { System.err.close(); System.setErr(System.out); }
当在构造函数中指定了initialCapacity
后,会先将threshold
赋值成所需的capacity
。所以先看tableSizeFor()
函数,它用来将指定的initialCapacity
转化成大于该值的2次幂。所以如果一开始capacity
的初始值是大于或等于0x40000000
的话,HashMap
指定的capacity
就是0x40000000
。源代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public HashMap (int initialCapacity) { this (initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap (int initialCapacity, float loadFactor) { if (initialCapacity < 0 ) throw new IllegalArgumentException ("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException ("Illegal load factor: " + loadFactor); this .loadFactor = loadFactor; this .threshold = tableSizeFor(initialCapacity); } static final int tableSizeFor (int cap) { int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1 ); return (n < 0 ) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1 ; }
②Map为空,但指定了初始大小 这种情况对应于上面代码中的HashMap<String, String> hashMap = new HashMap<>(capacity);
。这种情况下会将指this.threshold = tableSizeFor(initialCapacity);
,导致int oldThr = threshold;
能够获取到值,所以if (oldThr > 0)
成立,将newCap = oldThr
,实现了在第一次resize()
初始化数组时,按照指定的initialCapacity
new出数组。
③Map为空,未指定任何参数 这种情况使用默认参数:
1 2 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int )(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
得到newCap
和newThr
之后便开始初始化数组大小。初始化数组后,会将原数组中的Node
链表,按照某种规则迁移到新的数组上面去。
扩容后的位置调整 如果oldTab == null
,那么此次resize
属于第一次初始化HashMap
中的数组,直接返回刚new出来的新数组即可;当原数组中的第i
为存在Node
时,新的位置分布存在两种情况,即Node
有无后续Node
。这两种情况下,在原数组中的位置的计算方式是一样的,即:(n - 1) & hash
,其中n = tab.length
,所以位置也就是Node的hash值与上 length - 1
。
1 2 3 4 n = tab.length if ((p = tab[i = (n - 1 ) & hash]) == null ) tab[i] = newNode(hash, key, value, null );
无后续Node扩容后的位置 新的位置为:e.hash & (newCap - 1)
代码为:newTab[e.hash & (newCap - 1)] = e;
与之前的位置相比,可能是原位,可能是oldCap + 原位
有后续Node扩容后的位置 这条链表上面Node的hash值的与上oldCap
结果的含义是:**oldCap
的最高非0位对应在hash上的那一位到底是0还是1**。因为oldCap是0x400之类的值,只有最高位是1,其他位都是0。
如果结果是0,位置为:原位; 如果结果是1,位置为:原位+oldCap
只不过在新位置上之后,可能还是以一个链表的形式存在。
查询 - get() 我们先是通过常见的get(Object key)
方法来获取相应的值,该方法如下:
1 2 3 4 public V get (Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
这个方法接受两个参数,一个是kek的hash值,一个是key,在一定程度上可以加快速度吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 ; }
删除 - remove() 删除键值对,分两步,第一步是找到对应的键值对,第二步删除该键值对。删除时,可能会有两种情况(不考虑树结构),位于桶数组上或者位于后续的链表上。对于后者,因为此链表是单链表,我们需要将该键值对的前一个节点记录下来。
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 final Node<K,V> removeNode (int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1 ) & hash]) != null ) { Node<K,V> node = null , e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null ) { if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break ; } p = e; } while ((e = e.next) != null ); } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this , tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null ; }
后记 本文由本人的两篇对 HashMap 的学习总结拼凑而来,第一篇写于 2018 年,第二篇写于 2019 年,期间时隔一年,有一些明显的理解错误已经得到更正。现在是 2021 年,面对两篇曾经的笔记,仍然存在若干词不达意、表达不清、错别字的地方,部分地方是自己当前的理解。
非常感谢你的阅读。