jdk 源码系列之 HashMap
jdk 源码系列之 HashMap
JDK 源码系列
前言
了解 HashMap,理解扩容机制、数据结构、阈值以及初始化机制,对使用、优化等有所裨益。
继承关系
123public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {}
从这段代码,我们可知,HashMap 继承 AbstractMap 同时实现 Map 类。换句话说,Map 作为顶级父类,只定义方法,实现由子类实现,还继承 AbstractMap 那也是说,也可以调用 AbstractMap 里面的方法。
这里先摁住,还不清楚 HashMap 调了 AbstractMap 里面哪些方法。
HashMap
点进去先看看是数据结构是什么。
数据结构
/** * Basic hash bin node, used for most entries. (See below for * TreeNode subclass, and in LinkedHashMap for its Entry subclass.) */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; }}
向上继承 Map.Entry<K,V> 接口,并实现了 getKey getValue toString hashCode setValue equals 这六个方法。之后自己在实现一个链表 Node 的内部类,这个链表的节点都保存三个东西,hash key value 这三个变量。
接下来,我们从创建一个 HashMap 入手。
创建 HashMap
通常来讲,HashMap 有三种 new 的方式。
Map<String, String> first = new HashMap<>();Map<String, String> second = new HashMap<>(12);Map<String, String> third = new HashMap<>(first);Map<String, String> fourth = new HashMap<>(12 , 1);
第一种是无参构造,第二种是传入一个 int 值,第三种是允许传入一个 Map 实现的任何子类,第四种是两个 int 类型。
/** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */// first 的构造public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */// second 的构造public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR);}/** * Constructs a new <tt>HashMap</tt> with the same mappings as the * specified <tt>Map</tt>. The <tt>HashMap</tt> is created with * default load factor (0.75) and an initial capacity sufficient to * hold the mappings in the specified <tt>Map</tt>. * * @param m the map whose mappings are to be placed in this map * @throws NullPointerException if the specified map is null */// third 的构造public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false);}/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and load factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */// fourth 的构造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);}
这里提到三个变量,分别 initialCapacity、loadFactor、threshold。
看中文意思,分别是初始化容量、负载因子、阈值。其中负载因子,也就是 loadFactor 默认是 0.75f
/** * The load factor used when none specified in constructor. */static final float DEFAULT_LOAD_FACTOR = 0.75f;
在 third 这个构造函数里面,有个 putMapEntries 的方法。
/** * Implements Map.putAll and Map constructor. * 构造方法的时候就全部添加进去 hashMap * @param m the map 传入的Map * @param evict false when initially constructing this map, else * true (relayed to method afterNodeInsertion). 构造使用到就是 false 非构造就是 true */ final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { // Map 的大小 int s = m.size(); if (s > 0) { // 由于是构造调用的,table = null if (table == null) { // pre-size // 在调用次方法的时候,先赋值了 loadFactor = 0.75。所以ft = (Map 长度 / 0.75) + 1.0 float ft = ((float)s / loadFactor) + 1.0F; // 判断是否超出 MAXIMUM_CAPACITY = 1 << 30 也就是 2 的30次幂大小 int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); // 起始 threshold = 0,这个也在 fourth 的构造里面使用到了,下面有解释 if (t > threshold) // 如果 t 的大小在 2 的(n-1)次幂 与 2 的 n 次幂之间,则 t 的大小为 2 的n次幂。不懂没关系,下面有解释、举例 threshold = tableSizeFor(t); } // Map的长度 是否 大于 刚刚 tableSizeFor 之后的长度 // 首次是不会大于这个值的 else if (s > threshold) // 扩容,后面对这个方法讲解 resize(); // 将 Map 的子类对象 放进HashMap for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); // 先通过位运算减少 key 的冲突,在放进去 putVal(hash(key), key, value, false, evict); } } }
putMapEntries 这个方法就是 HashMap 在构造的时候,放进 Map 的对象,那么就会将他是所有 k v 都放进去,然后初始化长度,这个长度是当前 Map 长度靠近 2 n次幂 长度。比如
Map 7,则HashMap 初始化8,
如果是12 则是 16,
如果是16 则是 16.
之后判断是否需要扩容,由于是首次,所有不需要,接着就是放进 HashMap。先通过位运算计算 Hash 减少冲突,在pull。
/** * Computes key.hashCode() and spreads (XORs) higher bits of hash * to lower. Because the table uses power-of-two masking, sets of * hashes that vary only in bits above the current mask will * always collide. (Among known examples are sets of Float keys * holding consecutive whole numbers in small tables.) So we * apply a transform that spreads the impact of higher bits * downward. There is a tradeoff between speed, utility, and * quality of bit-spreading. Because many common sets of hashes * are already reasonably distributed (so don't benefit from * spreading), and because we use trees to handle large sets of * collisions in bins, we just XOR some shifted bits in the * cheapest possible way to reduce systematic lossage, as well as * to incorporate impact of the highest bits that would otherwise * never be used in index calculations because of table bounds. * * 通过位运算减少 Hash 冲突 */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
在 fourth 这个构造函数里面,还有一个这 tableSizeFor 的方法,上面有用到。
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;}
进行了位运算,由于前面好保证了 cap 非负数。
»> 右移,高位补零。比如 0001,使用 »> 1 之后就是 00001,对其长度,则1去掉。
**** 或操作,不同取1,相同1取1,相同0取0
tableSizeFor 方法里面的意思就是,如果 cap 介于 2 (n - 1)次幂 与2 n次幂之间,cap 的值为 2 n次幂 - 1 大小。
int n = 18;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;System.err.println(n); // 31int n = 16;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;System.err.println(n); // 31int n = 14;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;System.err.println(n); // 15
最后只要算的 n 非负数,且不大于 int 类型最大值,则 n + 1。也就是说这个初始容量,无论设置什么数字,都会是 2 的倍数。
HashMap 添加
我们常常使用 kv 的形式存储。
map.put("sin", "sy");
点进去看看,HashMap 如何存储。
// 首先来到父类定义的 V put(K key, V value); /** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or * <tt>null</tt> if there was no mapping for <tt>key</tt>. * (A <tt>null</tt> return can also indicate that the map * previously associated <tt>null</tt> with <tt>key</tt>.) * hashMap 的添加 */ // 然后实现父类定义的方法 public V put(K key, V value) { // hash 方法上面有提到作用 return putVal(hash(key), key, value, false, true); }
pull value 的时候,传入先经过位运算的 key 的 hashCode,然后传 key value、false,true
/** * Implements Map.put and related methods. * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value 这个值在pull 的时候是false * @param evict if false, the table is in creation mode. 这个值在pull 的时候是 true * @return previous value, or null if none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // 初始化两个容器 一个 k v 数组链表、一个是链表 Node<K,V>[] tab; Node<K,V> p; int n, i; // 第一次这里是null,所以 n = (tab = resize()).length; if ((tab = table) == null || (n = tab.length) == 0) // resize 下面有讲解 // 是否扩容处理,没有扩容,则是当前长度,有则是之前的两倍 n = (tab = resize()).length; // 因为数组从0开始计算,所以是 n - 1 取模,确定值放置在数值链表位置 // 如果当前数组链表为null,同时 p = 链表table if ((p = tab[i = (n - 1) & hash]) == null) // 创建一个链表 // 没有key值,就给你创建一个 tab[i] = newNode(hash, key, value, null); // 有值的情况下 else { // 数组链表头有数据 // 初始化 链表 泛型k Node<K,V> e; K k; // hash 值一致且key值的地址也一样 或者 key不为null 且key的值相同 // 大概就是相同key,替换value if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 如果 p 是 树结构 else if (p instanceof TreeNode) // 按树的形式 pul 进去 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 值不相同,也不是树结构,且链表头还相同,同一节点底下还有链表 else { // 向下遍历 for (int binCount = 0; ; ++binCount) { // 链表下一节点为null if ((e = p.next) == null) { // p的下一节点指向创建一个新的链表 p.next = newNode(hash, key, value, null); // 当链表的长度大于等于7,因为从1开始,也是说链表头有值,所以才从一开始。另外这是 ++1 性能比 i++好,i++比起++i多了一个创建临时变量(放入数据栈)的步骤,因此效率低一些 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // 链表转树结构 treeifyBin(tab, hash); break; } // key 值相同 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 当链表不为null,建立映射,kv if (e != null) { // existing mapping for key V oldValue = e.value; // onlyIfAbsent 如果是 true 不改变现有的值,false 改变,或者 oldValue == null if (!onlyIfAbsent || oldValue == null) // 完成关联 e.value = value; // 尾插,链表直接插到当前链表的最后一个位置上 afterNodeAccess(e); return oldValue; } } // 快速失败机制,比较当前操作次数与记录次数,是否不一样 ++modCount; // 因为完成一个 pulValue 所以增加一次次数,同时再次比较是否来到阈值 if (++size > threshold) // 来到阈值,2倍扩容容器 resize(); // true 如果是头结点则删除 afterNodeInsertion(evict); return null; } /** * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or move * with a power of two offset in the new table. * 具有初始化容器大小、2倍扩容容器等功能 * @return the table */ final Node<K,V>[] resize() { // 首次 table 是null Node<K,V>[] oldTab = table; // 判断 oldCap 是否为 null 为null 则为0,反之则是容器长度 int oldCap = (oldTab == null) ? 0 : oldTab.length; // 阈值 首次是0 int oldThr = threshold; // 新容器容量0 新容器阈值0 int newCap, newThr = 0; // 容量大于0 if (oldCap > 0) { // 大于等于最大能容纳的长度 if (oldCap >= MAXIMUM_CAPACITY) { // 阈值等于最大能容纳的长度 threshold = Integer.MAX_VALUE; return oldTab; } // 容器长度是否大于默认16,且新容器是旧容器的两倍大小且不大于最大容纳长度, else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 新阈值也是旧阈值的两倍大小 newThr = oldThr << 1; // double threshold } // 旧阈值 大于 0 else if (oldThr > 0) // initial capacity was placed in threshold // 新容器长度 = 旧阈值 newCap = oldThr; else { // zero initial threshold signifies using defaults // 首次 pull的时候 // 新容器长度 = 16 newCap = DEFAULT_INITIAL_CAPACITY; // 新阈值等于 负载因子 0.75 * 默认长度 16 = 12 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 这个判断只有在第一次使用构造方法传入一个 Map 才会用到,且这个Map 有值,其他是都基本走不到这里 if (newThr == 0) { // 容器长度 * 负载因子 0.75 float ft = (float)newCap * loadFactor; // 只要不大于最大容纳的长度,则 newThr = ft 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; // 旧容器不为null // 这是是首次 oldTab 为 null 不走这里,比如第一次 pull 构造方法放入map的子类 // 之后 pull 这里都会走一遍 if (oldTab != null) { // 链表循环指向下一个节点 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; // 先赋值,当前数组链表的链表给 e 链表且不为null if ((e = oldTab[j]) != null) { // 设置数组链表为null oldTab[j] = null; // 链表的下一节点为null if (e.next == null) // 只有数组链表头有数据,子节点没有数据 // hash 取模,这个模的长度是数组的长度,e 接到新的容器底下 newTab[e.hash & (newCap - 1)] = e; // 判断 e 是否是树结构 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // e 不是树结构,且数组链表的子节点也有数据 // 这里应该是计算不同hash的节点,到不同链表上 // 然后迁移到新的容器里,对应的数组链表的位置 else { // preserve order // Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; // 下一节点 Node<K,V> next; do { // e 指向下一节点 next = e.next; // e 的hash 取模 oldCap,分散。 if ((e.hash & oldCap) == 0) { // 第一次为null if (loTail == null) // loHead 是第一值 loHead = e; else // loTail的下一节点 指向 e的下一节点 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; } // 2倍扩容之后的数组长度,换句话说,之前的数据,如果 hash 取模之后不等于,之前最后面的位置,则一律变成之前的位置 加上 旧数组长度的。 // 如果之前是 2 数组链表长度是 8,之后这个位置来到 10 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
总结下,在 pulValue 的时候做了什么。
先初始化数据结构,数组链表
如果没有设置初始化大小,则默认长度为16,同时设置阈值为 0.75 * 当前长度
如果长度来到阈值大小,则进行当前容量二倍扩容,数据大部分会移到新的长度链表里面,少部分会遗留在旧容器长度里面
查看插入 key 是否存在,有则尾插入,插入之后是否来到 8 ,链表长度来到8则开始转成红黑树。没有,直接插入
然后 key 的位置里,在放入 value。完成关联
记录操作次数
插入后,在继续判断是否扩容
上面,少了链表如何转红黑树。接下来,我们继续看看
/** * Replaces all linked nodes in bin at index for given hash unless * table is too small, in which case resizes instead. */ final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // 链表长度是否小于 64,换句话说,树长最长只有64 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // 当链表长度,来到 8 的时候也会判断是否扩容 resize(); // 确定位置 else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { // 链表一个一个替换成树节点 TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) // 树节点,在转成红黑树,可以认为树节点拼接成一个树 hd.treeify(tab); } } /** * Forms tree of the nodes linked from this node. */ final void treeify(Node<K,V>[] tab) { TreeNode<K,V> root = null; for (TreeNode<K,V> x = this, next; x != null; x = next) { next = (TreeNode<K,V>)x.next; x.left = x.right = null; // 起始 red = false,当 = false 时候就是 黑色,同时根节放入值,只有首次才会走到 if (root == null) { x.parent = null; x.red = false; root = x; } else { K k = x.key; int h = x.hash; Class<?> kc = null; // 遍历链表 for (TreeNode<K,V> p = root;;) { int dir, ph; K pk = p.key; if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1; else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) dir = tieBreakOrder(k, pk); TreeNode<K,V> xp = p; if ((p = (dir <= 0) ? p.left : p.right) == null) { x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x; // 这里通过左旋 或者右旋,使得 黑的节点变成父节点,自旋完成后,父节点为黑色,而子节点都是红色 root = balanceInsertion(root, x); break; } } } } moveRootToFront(tab, root); }
转红黑树的过程中,首先会进行一次判断扩容处理,之后先变成一个个的树节点,在变成红黑树。
其中 red = false 时,为黑,反之则是红。通过左右旋,使其父节点变成黑色,子节点为红色,完成一次平衡处理。
图如下:
首次,root节点就是只有一个,所以直接是黑。当当前树高相差为3,会完成一次自旋,使其平衡,如果是不平衡的节点,则会变成红色,平衡的节点则为黑色。
HashMap 的数据结构图解
从上面,我们基本了解了,hashMap是怎么扩容的,如何链表转红黑树、如何定位位置。接下来我们看看HashMap的数据结构是如何。
起始的结构是由数组链表里面且是一个kv结构。头部是一个数组,k的位置是一个 hash 取模(位扰动之后的hash)之后,确定的位置。
后来当,hash冲突变多的时候,数组底下的链表就是存储 hash 冲突之后存储的位置,这个长度大于8的时候转红黑树,包括头,也就是数组,底下的链表长度是7。
优化以及使用 HashMap 的建议
既然读源码,就是为了更好的了解这个东西,只为读而读,为面试而读,这个源码读的没什么意思。学完得要有自己体会吧~
当使用的 HashMap 是固定不变或者未达到 2 的n次方长度,比如只使用到3,而当前 HashMap 长度为4,可以直接设置负载因子,固定其长度,避免过早的扩容,而浪费空间。
HashMap 默认初始化大小为16,如果添加的值,不到16的话,甚至只有它的一半,这样很容易造成空间上的浪费,之后过多的空间浪费,从而触发 GC 回收,降低整个系统的响应时间。所以尽可能的初始化大小,避免空间的浪费
当使用的 HashMap 长度超过16,都是没有初始化容器大小,来到阈值大小,频繁的触发扩容机制,导致 HashMap 性能下降。所以也尽可能的初始化大小,避免频繁触发扩容机制。
HashMap 线程不安全,这里主要是这两点(JDK8)通过源码我们知道,在添加元素的时候,在添加前以及添加后都会判断是否扩容,而改变 kv 的分布。在多线程中,如果 A 线程先添加完值,改变了整个容器值的分布,而 B 线程拿到的是原先容器大小来判断位置,从而导致插入的值不在正确的位置上。HashMap 里面有记录操作次数,在多线程中,当前的操作次数,与记录操作次数,可能会不正确,(可能拿到旧值)而出发快速失败机制。
只要是继承了 Map 接口的子类,都可以转成 HashMap,在创建对象的时候。
声明
作者: Sinsy
本文链接:https://blog.sincehub.cn/2020/10/31/jdk-hashmap/
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文声明。
如您有任何商业合作或者授权方面的协商,请给我留言:550569627@qq.com
引用
版权声明: 本文为 InfoQ 作者【苏格兰、情调】的原创文章。
原文链接:【http://xie.infoq.cn/article/cbc4ec349c9169053bcdc091e】。文章转载请联系作者。
苏格兰、情调
还未添加个人签名 2019.10.18 加入
还未添加个人简介
评论