HashMap深度分析(1.8版本为主)
一.HashMap概述
HashMap是基于哈希表的Map接口的非同步实现,允许null值和null键。而其本质上是一个“链表散列”的数据结构,即数组和链表的结合体。
JDK 1.8 HashMap 采用位桶 + 链表 + 红黑树实现。(当链表长度超过阈值 “8” 时,将链表转换为红黑树)
二.基本属性
// 默认容量16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 默认负载因子0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 链表节点转换红黑树节点的阈值, 9个节点转 static final int TREEIFY_THRESHOLD = 8; // 红黑树节点转换链表节点的阈值, 6个节点转 static final int UNTREEIFY_THRESHOLD = 6; // 转红黑树时, table的最小长度 static final int MIN_TREEIFY_CAPACITY = 64; // 链表节点, 继承自Entry static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; // ... ... } // 红黑树节点 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; // ... }
可以看出,Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。
三.HashMap存取实现
put方法
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; // 1.校验table是否为空或者length等于0,如果是则调用resize方法进行初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 2.通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增一个节点即可 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { // table表该索引位置不为空,则进行查找 Node<K,V> e; K k; // 3.判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 4.判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 5.走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数 for (int binCount = 0; ; ++binCount) { // 6.如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 7.校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点, // 减一是因为循环是从p节点的下一个节点开始的 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } // 8.如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; // 将p指向下一个节点 } } // 9.如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); // 用于LinkedHashMap return oldValue; } } ++modCount; // 10.如果插入节点后节点数超过阈值,则调用resize方法进行扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); // 用于LinkedHashMap return null; }
get方法
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; // 1.对table进行校验:table不为空 && table长度大于0 && // table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 2.检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; // 3.如果first不是目标节点,并且first的next节点不为空则继续遍历 if ((e = first.next) != null) { if (first instanceof TreeNode) // 4.如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { // 5.执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } // 6.找不到符合的返回空 return null; }
五.扩容机制实现
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; // 1.老表的容量不为0,即老表不为空 if (oldCap > 0) { // 1.1 判断老表的容量是否超过最大容量值:如果超过则将阈值设置为Integer.MAX_VALUE,并直接返回老表, // 此时oldCap * 2比Integer.MAX_VALUE大,因此无法进行重新分布,只是单纯的将阈值扩容到最大 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 1.2 将newCap赋值为oldCap的2倍,如果newCap<最大容量并且oldCap>=16, 则将新阈值设置为原来的两倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } // 2.如果老表的容量为0, 老表的阈值大于0, 是因为初始容量被放入阈值,则将新表的容量设置为老表的阈值 else if (oldThr > 0) newCap = oldThr; else { // 3.老表的容量为0, 老表的阈值为0,这种情况是没有传初始容量的new方法创建的空表,将阈值和容量设置为默认值 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 4.如果新表的阈值为空, 则通过新的容量*负载因子获得阈值 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } // 5.将当前阈值设置为刚计算出来的新的阈值,定义新表,容量为刚计算出来的新容量,将table设置为新定义的表。 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 6.如果老表不为空,则需遍历所有节点,将节点赋值给新表 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { // 将索引值为j的老表头节点赋值给e oldTab[j] = null; // 将老表的节点设置为空, 以便垃圾收集器回收空间 // 7.如果e.next为空, 则代表老表的该位置只有1个节点,计算新表的索引位置, 直接将该节点放在该位置 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; // 8.如果是红黑树节点,则进行红黑树的重hash分布(跟链表的hash分布基本相同) else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order // 9.如果是普通的链表节点,则进行普通的重hash分布 Node<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点 Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引位置+oldCap”的节点 Node<K,V> next; do { next = e.next; // 9.1 如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样 if ((e.hash & oldCap) == 0) { if (loTail == null) // 如果loTail为空, 代表该节点为第一个节点 loHead = e; // 则将loHead赋值为第一个节点 else loTail.next = e; // 否则将节点添加在loTail后面 loTail = e; // 并将loTail赋值为新增的节点 } // 9.2 如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap else { if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点 hiHead = e; // 则将hiHead赋值为第一个节点 else hiTail.next = e; // 否则将节点添加在hiTail后面 hiTail = e; // 并将hiTail赋值为新增的节点 } } while ((e = next) != null); // 10.如果loTail不为空(说明老表的数据有分布到新表上“原索引位置”的节点),则将最后一个节点 // 的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 11.如果hiTail不为空(说明老表的数据有分布到新表上“原索引+oldCap位置”的节点),则将最后 // 一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } // 12.返回新表 return newTab; }
六.哈希碰撞解决方法
当系统决定存储HashMap中的key-value对时,完全没有考虑Entry中的value,仅仅只是根据key来计算并决定每个Entry的存储位置。我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。
hash(int h)方法根据key的hashCode重新计算一次散列。此算法加入了高位计算,防止低位不变,高位变化时,造成的hash冲突。
static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
我们可以看到在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的 元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率。
对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 hash 码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,在HashMap中是这样做的:调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪个索引处。indexFor(int h, int length) 方法的代码如下:
static int indexFor(int h, int length) { return h & (length-1); }
这个方法非常巧妙,它通过 h & (table.length -1) 来得到该对象的保存位,而HashMap底层数组的长度总是 2 的 n 次方,这是HashMap在速度上的优化。在 HashMap 构造器中有如下代码:
这段代码保证初始化时HashMap的容量总是2的n次方,即底层数组的长度总是为2的n次方。
当length总是 2 的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
七.Fail-Fast机制
我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。
这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。
Hash Iterator() { expectedModCount = modCount; if (size > 0) { // advance to first entry Entry[] t = table; while (index < t.length && (next = t[index++]) == null) ; } }
在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map:
注意到modCount声明为volatile,保证线程之间修改的可见性。
final Entry<K,V> nextEntry() { if (modCount != expectedModCount) throw new ConcurrentModificationException();
在HashMap的API中指出:
由所有HashMap类的“collection 视图方法”所返回的迭代器都是快速失败的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。
注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。
参考文章:https://www.iteye.com/blog/zhangshixi-672697
https://blog.csdn.net/v123411739/article/details/78996181