Java集合之HashMap类源码剖析 一、HashMap类的简介 HashMap
是基于哈希表
的Map接口实现,此实现提供了所有可选的map操作,并允许null值
和null键
。HashMap类大致相当于Hashtable,只不过它是不同步
的并且允许nulls。该类不保证映射的顺序,特别是它不保证顺序随着时间的推移保持不变。
HashMap实例有两个影响其性能的参数:初始容量
和负载因子
。容量是哈希表中桶的数量,初始容量就是创建哈希表时的容量,负载因子是衡量哈希表在其容量自动增加之前允许达到的满度的指标。当哈希表中的条目数超过负载因子与当前容量的乘积时,哈希表将被重新哈希(即重建内部数据结构),使得哈希表的桶数大约为两倍。
作为一般规则,默认负载因子 (0.75) 在时间和空间成本之间提供了良好的权衡。较高的值会减少空间开销,但会增加查找成本(反映在HashMap类的大多数操作中,包括get和put)。 在设置其初始容量时应考虑映射中的预期条目数及其负载因子,以尽量减少重新哈希操作的次数。如果初始容量大于最大条目数除以负载因子,则不会发生重新哈希操作。如果要在HashMap实例中存储许多映射,那么创建一个足够大的容量将允许更有效地存储映射,而不是让它根据表的增长需要执行自动重新哈希(非常耗时的操作)。
请注意,此实现不是同步的。如果多个线程同时访问哈希表,并且至少有一个线程在结构上
修改了该哈希表,则必须进行外部同步。(结构修改是添加
或删除
一个或多个映射的任何操作;仅更改与实例已包含的键关联的值不是结构修改。)这通常是通过在自然封装映射的某个对象上进行同步来完成的。如果不存在这样的对象,则应使用Collections.synchronizedMap
方法“包装”映射。最好在创建时完成此操作,以防止意外地不同步访问Map:
1 Map m = Collections.synchronizedMap(new HashMap (...));
此类的所有“集合视图方法”返回的迭代器都是fail-fast
的:如果在创建迭代器后的任何时间对哈希表进行结构修改,除了通过迭代器自己的删除方法之外的任何方式,迭代器都会抛出ConcurrentModificationException。因此,面对并发修改,迭代器会快速而干净地失败
,而不是在未来不确定的时间冒任意、非确定性行为的风险。
二、HashMap类的底层结构 JDK8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK8以后的HashMap
在解决哈希冲突时有了较大的变化,当链表长度大于等于阈值(默认为8)(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。HashMap
默认的初始化大小为 16,之后每次扩充,容量变为原来的2倍。并且,HashMap
总是使用2的幂作为哈希表的大小。
三、HashMap类的常量字段 1 2 3 4 5 6 7 8 9 10 11 12 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 ;
上面定义的常量字段都是围绕哈希表的容量、负载因子和树化指标的,哈希表的默认初始化容量是16,默认负载因子是0.75,最大容量是2^30^,考虑链表长度增加会降低查找性能,因此如果链表长度超过8并且当前table数组长度不小于64(如果table数组长度还不足64的话,优先考虑数组扩容),就进行链表的树化。
四、HashMap类的静态工具 hash
方法计算key.hashCode()并将散列的高位扩展到低位做异或运算。由于哈希表大小始终是2的幂次,且最大为2^30^,因此仅在当前二进制位上方的位上变化的哈希集将始终发生冲突。因此,我们应用一种转换来向下分散较高位的影响
。
1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
tableSizeFor
方法根据指定的cap
参数计算该哈希表的容量,哈希表的容量就是散列数组table的长度,是大于等于cap
值的最小2的幂次(当然,不能超出最大容量限制)。
1 2 3 4 5 6 7 static final int tableSizeFor (int cap) { int num = -1 >>> Integer.numberOfLeadingZeros(cap - 1 ); return (num < 0 ) ? 1 : (num >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : num + 1 ; }
计算哈希数组长度的逻辑就是计算大于等于cap的最小2的幂次方,具体方法是通过计算前导零位数+二进制移位实现,例如对于一个二进制n位的数字其前导零位数就是32-n,那么-1(二进制表示形式为全1)无符号右移32-n位得到低位为连续n位的1的数字,该数字加一后就是大于等于cap的最小2的幂次方。不过需要注意cap本身就是2的幂次方的情况,此时按照这种计算方式得不到最小的2幂次方,因此需要统一做cap-1的处理!
五、HashMap类的重要内部类 Node
是继承自Map.Entry
接口的静态内部类,是哈希表中最重要的基本内部类,封装了键的哈希值hash、键key、值value、Node的后继节点next这四个属性字段,并实现了Map.Entry
接口的方法。
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 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) { this .hash = hash; this .key = key; this .value = value; this .next = next; } public final K getKey () { return key; } public final V getValue () { return value; } public final String toString () { return key + "=" + value; } public final V setValue (V newValue) { V oldValue = value; value = newValue; return oldValue; } public final int hashCode () { return Objects.hashCode(key) ^ Objects.hashCode(value); } 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 ; } }
六、HashMap类的重要字段 1 2 3 4 5 6 7 8 9 10 transient Node<K,V>[] table;transient int size;transient int modCount;int threshold;final float loadFactor;
HashMap主体就是一个散列数组,其中数组的每个元素都可能是一个单链表或红黑树,HashMap中最重要的两个字段就是扩容阈值和负载因子,而且需要注意:出于性能考虑散列数组推迟到第一次使用时才会初始化
,这种延迟初始化的思想很多地方都会使用到,如操作系统的写时复制、Spring中Bean对象的懒加载、MyBatis中分步查询的懒加载、AQS同步队列的延迟初始化等等。
七、HashMap类的构造方法 HashMap类的构造方法有四个,首先前两个属于同一种情况,只是简单的复用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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); } public HashMap (int initialCapacity) { this (initialCapacity, DEFAULT_LOAD_FACTOR); }
不过,我们最常用的还是HashMap的无参构造函数,构造的HashMap实例拥有默认初始容量16和默认负载因子0.75,在后面resize
方法进行哈希表初始化时会看到通过无参构造函数创建的HashMap对象的threshold为0,初始化散列数组时长度为默认初始化容量16。
1 2 3 public HashMap () { this .loadFactor = DEFAULT_LOAD_FACTOR; }
最后,我们也不得不提拷贝构造函数,其负载因子为默认的0.75,初始容量足以容纳Map对象的所有映射键值对,其中putMapEntries的逻辑需要等最后才说。
1 2 3 4 public HashMap (Map<? extends K, ? extends V> m) { this .loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false ); }
八、HashMap类的put方法
首先看一下JDK注释:插入指定的键值对,如果键已经存在,则用新值更新与之关联的旧值。返回与键关联的旧值,如果不存在则返回null(注意,null同时也能表明与键关联的值就是null值,这在HashMap中是允许的)。
下面,我们重点关注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 61 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 ; }
需要关注的是,哈希表的初始化过程会延迟到第一次调用put方法时,对于哈希表中不存在键匹配的Node的情况,需要往桶位链表末尾添加新键值对Node,并且可能会树化(链表长度达到9且散列数组长度不小于64);对于哈希表中存在键匹配的Node的情况,需要根据onlyIfAbsent参数决定是否覆盖更新。
九、HashMap类的resize方法 resize
方法内处理初始化逻辑和哈希表扩容逻辑,扩容策略是在不超过最大容量限制的情况下直接扩容为原来的两倍 ,当散列数组的长度达到最大容量限制时无法再继续扩容,设置扩容阈值为Integer.MAX_VALUE。对于扩容逻辑,哈希表需要Rehash运算:
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 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; }
十、HashMap类的get方法
重点的还是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 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 ; }
十一、HashMap类的remove方法
重点的还是removeNode
方法,
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 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类的entrySet方法 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 public Set<Map.Entry<K,V>> entrySet() { Set<Map.Entry<K,V>> es; return (es = entrySet) == null ? (entrySet = new EntrySet ()) : es; } final class EntrySet extends AbstractSet <Map.Entry<K,V>> { public final int size () { return size; } public final void clear () { HashMap.this .clear(); } public final Iterator<Map.Entry<K,V>> iterator() { return new EntryIterator (); } public final boolean contains (Object o) { if (!(o instanceof Map.Entry)) return false ; Map.Entry<?,?> e = (Map.Entry<?,?>) o; Object key = e.getKey(); Node<K,V> candidate = getNode(hash(key), key); return candidate != null && candidate.equals(e); } public final boolean remove (Object o) { if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>) o; Object key = e.getKey(); Object value = e.getValue(); return removeNode(hash(key), key, value, true , true ) != null ; } return false ; } public final void forEach (Consumer<? super Map.Entry<K,V>> action) { Node<K,V>[] tab; if (action == null ) throw new NullPointerException (); if (size > 0 && (tab = table) != null ) { int mc = modCount; for (Node<K,V> e : tab) { for (; e != null ; e = e.next) action.accept(e); } if (modCount != mc) throw new ConcurrentModificationException (); } } }
根据上面的代码可知,entrySet方法返回的Set集合直接操作的是关联的Map对象,我们重点看一下entrySet迭代器的实现:
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 abstract class HashIterator { Node<K,V> next; Node<K,V> current; int expectedModCount; int index; HashIterator() { expectedModCount = modCount; Node<K,V>[] t = table; current = next = null ; index = 0 ; if (t != null && size > 0 ) { do {} while (index < t.length && (next = t[index++]) == null ); } } public final boolean hasNext () { return next != null ; } final Node<K,V> nextNode () { Node<K,V>[] t; Node<K,V> e = next; if (modCount != expectedModCount) throw new ConcurrentModificationException (); if (e == null ) throw new NoSuchElementException (); if ((next = (current = e).next) == null && (t = table) != null ) { do {} while (index < t.length && (next = t[index++]) == null ); } return e; } public final void remove () { Node<K,V> p = current; if (p == null ) throw new IllegalStateException (); if (modCount != expectedModCount) throw new ConcurrentModificationException (); current = null ; removeNode(p.hash, p.key, null , false , false ); expectedModCount = modCount; } } final class EntryIterator extends HashIterator implements Iterator <Map.Entry<K,V>> { public final Map.Entry<K,V> next () { return nextNode(); } }
可见,HashMap的EntryIterator遍历的顺序是散列数组从前往后,其中散列数组每个元素对应的单链表也是从前往后。
十三、HashMap类的其他方法
十四、HashMap类的一点小补充
HashMap的字段中出现了一个Java关键字transient
,它有什么作用嘞?
首先,transient修饰的字段不参与序列化,因为HashMap内部的散列数组可能存在未使用的桶位,没有被使用到的空间被序列化没有意义,所以需要手动使用 writeObject() 方法,只序列化实际存储元素 。
更重要的是,HashMap的底层是一些包含“键值”项的散列桶数组,到底一个项(key)被放在哪个桶中,这是该键的hashCode确定的(准确说是经过扰动的hash值),一般情况下,不同的JVM实现不保证会有相同的hashCode。实际上,即使是相同的JVM实现中,也无法保证每次运行都一样。 因此,对于HashMap而言,接受默认的序列化形式将会导致一个严重的bug:对HashMap对象进行序列化和反序列化操作所产生的的对象,其约束关系会遭到严重的破坏。
实际上,HashMap的序列化与反序列化都是使用writeObject和readObject完成的,其中readObject方法会针对每一个Entry调用putVal重新从头构建HashMap对象,因为hashCode不能保证与序列化前的一致。
毫无疑问,HashMap就是线程不安全的,因为内部没有使用任何同步措施如synchronized
、CAS
等等,举例来说:当HashMap进行扩容时,其实就是把旧散列表中每个桶位的元素(链表或红黑树)做ReHash操作到新散列表中,如果在这个过程中新插入一个Entry或删除了一个Entry,并且这个Entry所在的散列表桶位已经完成了同步迁移,那么这次的修改就无法同步
到新散列表中,由此产生了线程安全问题!