Jdk1.8 HashMap中put()方法源码分析

it2023-03-13  83

1.说明下环境,为jdk1.8,仅对put方法进行源码分析。 2.特别说明:我使用的是这个构造器:

//15表示自定义map的初始容量为15 //0.7f表示自定义负载因子为0.7 HashMap<String, String> mapMine = new HashMap<String, String>(15,0.7f);

并不是下面的构造器:

HashMap<String, String> mapMine = new HashMap<String, String>();

使用第一个构造器,而不实用第二个构造器的原因是想让源码分析的更充分些,如果使用第二个构造器,那么默认的负载因子为0.75f,默认的table(下面会详细说到)数组的长度为16。而使用第一个构造器可以自由为之。 下面为测试代码,十分简单:

package com.yuhl.map; import java.util.HashMap; /** * @author yuhl * @Date 2020/10/19 23:02 * @Classname HashMap * @Description 剖析HashMap源码 */ public class HashMapDemp { public static void main(String[] args) { HashMap<String, String> mapMine = new HashMap<String, String>(15,0.7f); mapMine.put("洛阳1", "洛阳洛阳1"); mapMine.put("洛阳2", "洛阳洛阳2"); mapMine.put("洛阳3", "洛阳洛阳3"); mapMine.put("洛阳4", "洛阳洛阳4"); mapMine.put("洛阳5", "洛阳洛阳5"); mapMine.put("洛阳6", "洛阳洛阳6"); mapMine.put("洛阳7", "洛阳洛阳7"); mapMine.put("洛阳8", "洛阳洛阳8"); mapMine.put("洛阳9", "洛阳洛阳9"); mapMine.put("洛阳10", "洛阳洛阳10"); mapMine.put("洛阳11", "洛阳洛阳11"); mapMine.put("洛阳12", "洛阳洛阳12"); mapMine.put("洛阳13", "洛阳洛阳13"); mapMine.put("洛阳14", "洛阳洛阳14"); mapMine.put("洛阳15", "洛阳洛阳15"); mapMine.put("洛阳16", "洛阳洛阳16"); mapMine.put("洛阳17", "洛阳洛阳17"); } }

3.构建map对象,HashMap<String, String> mapMine = new HashMap<String, String>(15,0.7f); 源码如下:

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); }

源码分析,

this.loadFactor = loadFactor;

表示把我自定义的复杂因子0.7f赋值给成员变量loadFactor ,待后面扩容时作为乘子使用。 看下面一行代码:

this.threshold = tableSizeFor(initialCapacity);

意为把tableSizeFor(initialCapacity);返回的值赋值给threshold (扩容阈值),tableSizeFor(initialCapacity)的作用就是拿到构造器中的第一个参数然后通过一些列计算得到一个比initialCapacity大的最小2的幂次值,即如果构造器第一个参数和次方法返回值的关系:

第一个参数的值tableSizeFor(initialCapacity)方法返回值1258101622325064

通过这个表规律应该很明显了吧。下面我看进入这个方法看一下:

/** * Returns a power of two size for the given target capacity. */ 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; }

他别解释: int n = cap - 1; //减去1的摸底就为了规避,你输入的就是2的幂次。 每右移一位为除以2。 那么为什么数组长度为2的n次幂呢?因为不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。 想了解原因的可以自行深入了解了。此处不再展开。 本例子中第一个构造器为15所以可以知道tableSizeFor(int cap)的返回值为16。有的同学可以会提出异议:不是设置的是容量吗?为什么值会放在扩容的阈值上呢?在此处,阈值仅是为别人做嫁衣,仅仅是借此一用而已,后面就会真正赋值给capacity了。为什么要这样呢?可能设计者们思考的是少定义一个变量,同时可以和无惨的构造器共用代码。这可能就是大牛的提现吧。 至此: loadFactor =0.7f; threshold = 16; map对象构建完成。 4.put第一行代码的执行过程:mapMine.put(“洛阳1”, “洛阳洛阳1”);

public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }

此处需要先执行hash(key)方法,即hash(“洛阳1”),我们进入代码看下他是怎么计算这个hash的:

static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }

插一句题外话:此处可以看到如果我们的key为null,也是可以的,会把null的hash设置为0,这也是为什么HashMap可以存入key=null的原因。题外话插入结束。 接下来看下key.hashCode()方法:

public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }

注意1:为什么用31作为乘子,原因就是为了让hash的分布的更为均匀,仅此:关于此问题可以参考下面链接:https://blog.csdn.net/happydecai/article/details/80493237添加链接描述 注意:2:31 * h + val[i];拥有几个字符就会for几次。目的仍然为让hash分布。 此时成员变量hash=28033810; 获得hash值后开始调用 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) 方法:

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) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }

这个方法里面可以需要注意的点为: 注意1:执行n = (tab = resize()).length;方法:

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; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults 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 { // preserve order 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; }

主要看:

newCap = oldThr;//把老的阈值赋值给新的容量值了。这就是前面解释的为什么要把map构造器中的值先放入threshold的原因。

newCap = oldThr;//把老的阈值赋值给新的容量值了。这就是前面解释的为什么要把map构造器中的值先放入threshold的原因。 注意2: threshold = 15 * 0.7f=11.2即如果容量达到11时就要扩容了。 代码如下:

float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);

注意三:初始容量大小为16. 源码如下:

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab;

resize()方法执行完成后,我们拥有了一个transient Node<K,V>[] table,如下图所示: 下面构建Node节点:

if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);

newNode(hash, key, value, null);源码:

// Create a regular (non-tree) node Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) { return new Node<>(hash, key, value, next); }

调用:

/** hash=28033810 key=洛阳1 value=洛阳洛阳1 next=null */ Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }

i = (n - 1) & hash算出的值为:2 即需要放入索引为2的头位置: 结果如下: 下面看:

++modCount;

表示map扩容和更改次数,是一个计数器。 下面看:

if (++size > threshold) resize();

size表示map中的元素个数,与于本例子如果大于11则需要嗲用resize()重新扩容。后面会说到再次扩容。 下面对:

mapMine.put("洛阳2", "洛阳洛阳2"); mapMine.put("洛阳3", "洛阳洛阳3"); mapMine.put("洛阳4", "洛阳洛阳4"); mapMine.put("洛阳5", "洛阳洛阳5"); mapMine.put("洛阳6", "洛阳洛阳6"); mapMine.put("洛阳7", "洛阳洛阳7"); mapMine.put("洛阳8", "洛阳洛阳8"); mapMine.put("洛阳9", "洛阳洛阳9");

进行操作和对

mapMine.put("洛阳2", "洛阳洛阳2");

操作流程一模一样,不在赘述。 此时table的结构如下:

5.重点来了。当执行:

mapMine.put("洛阳10", "洛阳洛阳10");

的时候有个新情况出现了,当执行到下面方法的时候:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)

执行if()的条件判断,(n - 1) & hash的值为11,而在11的位置上已经有了一个node存在,即:洛阳8,此时需要走else代码逻辑:

if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; }

即把当前的节点挂在洛阳8节点的下面: 同样的情况出现在执行:

mapMine.put("洛阳11", "洛阳洛阳11"); mapMine.put("洛阳12", "洛阳洛阳12");

的时候,所以目前情况是这样的: 但是洛阳12加入后出现了需要扩容的情况,因为我们扩容阈值为11。 当调用

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)

方法,前半部分执行完成后,洛阳12挂在了洛阳6的下面,紧跟着就是扩容的判断代码:

if (++size > threshold) resize();

++size 为12,大于11,需要热size()了。 6.当拥有16的容量,第一次次扩容代码分析:

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; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults 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 { // preserve order 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; }

先说下扩容后的各个值的变化情况和tabl和链表的结构: capacity = 32 增加一倍 threshold = 22 增加一倍 下面分别介绍一下为什么? 问题1:capacity = 32 增加一倍?

newCap = oldCap << 1 //左移一位增加一倍

问题2:threshold = 22 增加一倍

newThr = oldThr << 1;//左移一位增加一倍

问题3:在resize()的时候,原来已经在table中的node该怎么变化,以及这些node后面的node怎么变化。分三种情况: 第一种情况:原位置不变如:index 为120和11的情况。 原因为:

if (e.next == null) newTab[e.hash & (newCap - 1)] = e;

即无next=null是会做一个判断e.hash & (newCap - 1)正好与原来的位子一致则无需改动。 另外一个:

if (loTail != null) { loTail.next = null; newTab[j] = loHead; }

这个位置在判断是满足loTail != null,就放在原来的位置。 第二种情况:在原来的位置索引+原来的容量的位置: 出现这两种请款的原因就是他的hash算法倒置的。故意为之。具在这里插入代码片体原因可以另行脑补,这里不再展开。

至次,put完成。

7树化

{ for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } }

当binCount即放入元素后进行链表长度的判断,如果大于等于TREEIFY_THRESHOLD , 转为红黑树。提高查询效率。 但是在树化方法调用时:

final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 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); } }

MIN_TREEIFY_CAPACITY=64//最小树化值,如果不满足这个条件,则不会树化。这个长度不是链表的长度,而是hash数组的长度即:transient Node<K,V>[] table的长度。 到此put结束。

前路无畏 认证博客专家 专家认证 自律的艰辛总甜过懊悔的苦果!专注于java后端技术及解决方案,善于总结,分享!自律的艰辛总甜过懊悔的苦果!专注于java后端技术及解决方案,善于总结,分享!自律的艰辛总甜过懊悔的苦果!专注于java后端技术及解决方案,善于总结,分享!
最新回复(0)