JDK1.8HashMap源码分析
概要
HashMap允许key为null ,value为null ,由于是hash存放,所以遍历时无序。
底层的数组称为哈希桶 ,每个桶(数组元素)存放的是链表,链表中每个结点就是哈希表的元素
在JDK8中,当链表长度达到8 ,会转化成红黑树 ,以提升它的查询、插入效率 ,它实现了Map<K,V>
,
Cloneable
, Serializable
接口。
因其底层哈希桶的数据结构是数组 ,所以也会涉及到扩容 的问题。
当HashMap的容量达到threshold
阈值时,就会触发扩容。扩容前后,哈希桶的长度一定会是2的n次方 。
这样在根据key的hash值寻找对应的哈希桶时,可以用位运算替代取余操作 ,更加高效。
而key的hash值,并不仅仅只是key对象的hashCode()方法的返回值,还会经过扰动函数 的扰动,以使hash值更加均衡 。
因为hashCode()是int类型,取值范围是40多亿,只要哈希函数映射的比较均匀松散,碰撞几率是很小的。
但就算原本的hashCode()取得很好,每个key的hashCode()不同,但是由于HashMap的哈希桶的长度远比hash取值范围小,默认是16,所以当对hash值以桶的长度取余,以找到存放该key的桶的下标时,由于取余是通过与操作 完成的,会忽略hash值的高位 。因此只有hashCode()的低位 参加运算,产生不同的hash值,但是得到的index相同的情况的几率会大大增加,这种情况称之为hash碰撞。
即,碰撞率会增大。
扰动函数就是为了解决hash碰撞 的。它会综合hash值高位和低位的特征,并存放在低位,因此在与运算时,相当于高低位一起参与了运算,以减少hash碰撞的概率 。(在JDK8之前,扰动函数会扰动四次,JDK8简化了这个操作)
扩容操作时,会new一个新的Node数组 作为哈希桶,然后将原哈希表中的所有数据(Node节点)移动到新的哈希桶中,相当于对原哈希表中所有的数据重新做了一个put操作。所以性能消耗很大 ,可想而知,在哈希表的容量越大时,性能消耗越明显。
扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。
因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位,
或者扩容后的下标,即high位。 high位= low位+原哈希桶容量
如果追加节点后,链表数量>=8,则转化为红黑树
由迭代器的实现可以看出,遍历HashMap时,顺序是按照哈希桶从低到高 ,链表从前往后 ,依次遍历的。属于无序集合。
HashMap的源码中,充斥个各种位运算代替常规运算的地方,以提升效率: *
与运算替代模运算。用 hash & (table.length-1)
替代
hash % (table.length)
*
用if ((e.hash & oldCap) == 0)
判断扩容后,节点e处于低区还是高区。
本文部分图片来源于网络,侵删。
JDK1.7的HashMap
jdk1.7的hashmap采用数组+链表的数据结构,对于Hash碰撞冲突的解决方法采用的是链表头插移动法 。
jdk1.7数据结构
JDK1.7与JDK1.8HashMap的区别:
1.7是数组+链表,1.8则是数组+链表+红黑树结构
jdk1.7中当哈希表为空时,会先调用inflateTable()
初始化一个数组;而1.8则是直接调用resize()
扩容;
插入键值对的put方法的区别,1.8中采用尾插法 ,而1.7中是采用头插法 ;
jdk1.7中的hash函数对哈希值的计算直接使用 key的hashCode值,而1.8中则是采用key的hashCode异或上key的hashCode进行无符号右移16位的结果,避免了只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,使元素分布更均匀;
扩容时1.8会保持 原链表的顺序,而1.7会颠倒 链表的顺序;而且1.8是在元素插入后 检测是否需要扩容,1.7则是在元素插入前;
扩容策略:1.7中是只要不小于 阈值就直接扩容2倍 ;而1.8的扩容策略会更优化,当数组容量未达到64时,以2倍进行扩容,超过64之后若桶中元素个数不小于7 就将链表转换为红黑树,但如果红黑树中的元素个数小于6就会还原为链表 ,当红黑树中元素不小于32 的时候才会再次扩容。
JDK1.8的HashMap
JDK1.8采用的是数组+链表+红黑树
img
Map的实际数量=threshold容量阈值时,会进行两倍 扩容
Map中某个桶的链表长度大于 树形化阈值TREEIFY_THRESHOLD=8
,但Map元素数量小于 树形化最小容量MIN_TREEIFY_CAPACITY=64
时,会进行两倍扩容(不会转化为红黑树)
Map中某个桶链表长度大于 树形化阈值TREEIFY_THRESHOLD=8
,且Map元素数量大于 树形化最小容量MIN_TREEIFY_CAPACITY=64
时,会将链表转化 为红黑树。
结构认识
image-20210613111007848
HashMap仅仅实现了常规的Serializable
、Cloneable
、Map
等接口,继承了AbstractMap
抽象类。
可以看到里面的字段非常多,我们稍后一一探讨。
源码解析
我们还是按照我们字段解释->构造函数解释->主要方法解释
的流程来进行。主要内容基本都在代码注释里,重点看注释 !!!
字段解释
HashMap里面的字段很多,包括各种阈值以及Map的属性。每个字段上面都有英文注释,可以帮助我们很好地去理解。
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 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;transient Set<Map.Entry<K,V>> entrySet;transient int size;transient int modCount;int threshold;
存储数据元素是用Node
来存储,包含了hash值
、key
、value
、next
指针。
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 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 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 ; } }
构造函数解释
了解了基本的字段意义后我们来看看HashMap的构造函数。HashMap有4个构造函数
image-20210613113742329
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 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); }public HashMap () { this .loadFactor = DEFAULT_LOAD_FACTOR; }public HashMap (Map<? extends K, ? extends V> m) { this .loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false ); }
无论是创建哈希表还是ArrayList,初始时能够手动指定大小就尽量手动指定大小。
主要方法解释
先看一些比较重要的方法,非接口方法。
计算hash值(key的hashcode无符号右移16位后,跟原来的hashcode做异或运算)
static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
我们看一个例子
img
右移16位相当于把高16位移到低16位,高16位置为0 ,跟原来的hashcode异或后,结果高16位保持不变,低16位等于原来的高16位与低16位进行异或 。
即异或后高16位没有变,只是低16位变了。
为什么要这么做呢?
我们要知道这个函数的目的是计算出散列更均匀、碰撞几率更小 的hash值,所以这样做能够达到较好的效果。为什么能达到这样的效果?
计算出来的hash值会用于计算元素存储的桶位 ,计算公式为(capacity - 1) & hash
,如果数组现在有16个桶位(槽位slot),那么计算后结果如下:
img
发现了吗?数组容量为16时,高16位全部为0!如果我们不将原来的hashcode右移16位,那么hashcode高16位就完全没有参与计算 ,相当于完全被抛弃了。只采用低16位进行计算产生Hash冲突的概率就会增加很多!
到这里也可以理解为什么容量必须是2的n次方了。因为2^n - 1
可以保证产生最多的1,从而计算槽位时可以保证尽量根据hashcode的不同而均匀地散列。如果非2^n,那么会导致0非常多或者1非常多,结果也就会趋于0或着最大值 ,这样产生hash冲突的概率会非常大!
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的所有元素加入表中,参数evict初始化时为false,其他情况为true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 final void putMapEntries (Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0 ) { if (table == null ) { float ft = ((float )s / loadFactor) + 1.0F ; int t = ((ft < (float )MAXIMUM_CAPACITY) ? (int )ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t); } else if (s > threshold) resize(); for (Map.Entry<? extends K , ? extends V > e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false , evict); } } }
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 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; }
扩容方法比较复杂,其中比较复杂的地方在于将旧链表拆分成两条新的链表 ,通过
e.hash & oldCap
来计算新链表在扩容后的数组中的新下标。
当
e.hash & oldCap = 0
,则节点在新数组中的索引值与旧索引值相同 。
当e.hash & oldCap = 1
,则节点在新数组中的索引值为旧索引值+旧数组容量 。
为什么可以用位运算代替取模运算呢?
可以推导出e.hash & (capacity - 1) = e.hash % capacity
,具体推导可以查看博客
HashMap
中的取模和扩容公式推导_Sumkor的博客-CSDN博客_hashmap 取模
为什么结果为0的时候与旧索引值相同,而结果为1则不同呢?
假设原来哈希值为15即1111,由于容量翻倍,哈希值位数也增加一位,所以重新计算,如果下图中b位为0,那么计算出来的新哈希值为01111,哈希值不变 。如果b位为1,计算出来的新哈希值为11111即31,刚好等于旧索引值(15)+旧数组容量(16) 。
image-20210613144933704
步骤总结如下:
根据旧容量和旧阈值计算新容量和新阈值。
创建新哈希表,对旧哈希表进行遍历
如果元素不为null,重新计算元素的哈希值,存放到新哈希表中(低位链表和高位链表)
如果发生哈希冲突,且结点超过8个,转化为红黑树
返回新哈希表
putVal()
,!!重点!!
onlyIfAbsent=true
表示不会覆盖已存在key的value,evict=false
表示在初始化时调用。
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 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) { 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操作的流程图,觉得很清晰,放在下面
put操作流程
如果觉得看不明白还有一张,侵删
HasMap存储原理
情况总结如下:
如果对应桶位置为空 ,直接插入
如果对应桶位置存在元素,发生哈希冲突
如果存在 相同key,不需要新插入,直接覆盖
如果不存在相同key,判断是否为树结点
如果非红黑树 结构,插到链表尾部,插入后如果长度大于8,转为红黑树
如果为红黑树结构,插入到红黑树
put操作至此应该解读得比较清晰了
删除元素remove()
,matchValue=false
:不要求必须key、value都相等才可删除,movable=true
:删除元素后,可以移动其他结点
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 public V remove (Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null , false , true )) == null ? null : e.value; }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 ; }
remove操作也有一张流程图:
img
按照流程图可以对照源码进行研究。
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 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 ; }
流程图如下:
img
jdk1.8新增方法
以key为条件,找到了返回value。否则返回defaultValue
@Override public V getOrDefault (Object key, V defaultValue) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? defaultValue : e.value; }
我们用EntrySet一般是为了获取Iterator
public Set<Map.Entry<K,V>> entrySet() { Set<Map.Entry<K,V>> es; return (es = entrySet) == null ? (entrySet = new EntrySet ()) : es; }
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 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 Spliterator<Map.Entry<K,V>> spliterator() { return new EntrySpliterator <>(HashMap.this , 0 , -1 , 0 , 0 ); } 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 (); } } }
另外HashMap还提供了很多迭代器,Splitearot:可分割迭代器,为了并行遍历数据源中的元素而设计的迭代器,这个可以类比最早Java提供的顺序遍历迭代器Iterator,但一个是顺序遍历,一个是并行遍历。
image-20210613154045660
为什么有了Iterator还要Spliterator呢?
从最早Java提供顺序遍历迭代器Iterator时,那个时候还是单核时代,但现在多核时代下,顺序遍历已经不能满足需求了,如何把多个任务分配到不同核上并行执行,才是能最大发挥多核的能力,所以产生了Spliterator
补充
HashMap和HashTable的区别
与之相比HashTable
是线程安全的,且不允许key、value是null。
HashTable
默认容量是11。
HashTable
是直接使用key的hashCode(key.hashCode())
作为hash值,不像HashMap内部使用static final int hash(Object key)
扰动函数对key的hashCode
进行扰动后作为hash值。
HashTable
取哈希桶下标是直接用模运算%.(因为其默认容量也不是2的n次方。所以也无法用位运算替代模运算)
扩容时,新容量是原来的2倍+1。int newCapacity = (oldCapacity << 1) + 1;
Hashtable是Dictionary
的子类同时也实现了Map接口,HashMap是Map接口的一个实现类;
小结
文中关于红黑树的具体操作部分没有展开描述,具体想了解的读者可自行查阅资料。
阅读HashMap源码的过程中不仅仅学习到了大佬代码的思考,更有一种亲身体会大佬们在设计和实现这些想法时候的取舍。对于我来说源码阅读能力自我感觉也有一定提升,当然还有一些细节部分没有具体去探究,只求能将学习到的知识进行内化固化就可!