热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

HashMap的初始化与扩容

原文链接:通俗易懂Hashmap源码解析_java阳开发之路的博客-CSDN博客_hashmap源码https:www.csdn.nettagsMtTaEgxsN

原文链接:

通俗易懂Hashmap源码解析_java阳开发之路的博客-CSDN博客_hashmap源码

https://www.csdn.net/tags/MtTaEgxsNTYxMDAyLWJsb2cO0O0O.html

深入理解 HashMap put 方法(JDK 8逐行剖析)_stateiso的博客-CSDN博客_hashmap put方法

·

·


一, 先来看几个变量、常量、静态变量、静态常量:


详情参见:HashMap的结构与构造函数_Morning sunshine的博客-CSDN博客



1) Map的默认初始化容量以及容量极限:

//HashMap默认的初始容量大小--16&#xff0c;容量必须是2的幂static final int DEFAULT_INITIAL_CAPACITY &#61; 1 <<4; // aka 16//HashMap的容量极限&#xff0c;为2的30次幂&#xff1b;static final int MAXIMUM_CAPACITY &#61; 1 <<30;

2&#xff09; 默认负载因子、实际元素数量&#xff1a;

//负载因子的默认大小&#xff0c;//元素的数量/容量得到的实际负载数值与负载因子进行对比&#xff0c;来决定容量的大小以及是否扩容&#xff1b;static final float DEFAULT_LOAD_FACTOR &#61; 0.75f;//HashMap的实际元素数量transient int size;

·


3&#xff09;table——Entry数组&#xff1b;

//Node是Map.Entry接口的实现类&#xff0c;可以将这个table理解为是一个entry数组&#xff1b;//每一个Node即entry&#xff0c;本质都是一个单向链表transient Node[] table;

· 


4&#xff09;Map已经修改的次数、扩容阀值、存储负载因子的常量&#xff1a;

//HashMap已在结构上修改的次数 结构修改是指更改 HashMap 中的映射数量或以其他方式修改其内部结构&#xff08;例如&#xff0c;重新散列&#xff09;的那些transient int modCount;//下一次HashMap扩容的阀值大小&#xff0c;如果尚未扩容&#xff0c;则该字段保存初始entry数组的容量&#xff0c;或用零表示int threshold;//存储负载因子的常量&#xff0c;初始化的时候将默认的负载因子赋值给它&#xff1b;final float loadFactor;

        扩容阀值threshold &#61; 负载因子loadFactor * 容量capacity

·


二&#xff0c;HashMap的四个构造函数&#xff1a;


1&#xff09;构造函数1——默认的无参构造函数&#xff1a;

        将默认的负载因子0.75f 传给 存储负载因子的常量&#xff1b;

//默认的构造函数public HashMap() {//将默认的负载因子0.75f传给存储负载因子的常量this.loadFactor &#61; DEFAULT_LOAD_FACTOR; }

2&#xff09;构造函数2——带指定容量参数的构造函数

        将默认的负载因子0.75f当做负载因子常量&#xff0c;连同用户传入的容量参数&#xff0c;一起传给两个参数都带的构造函数&#xff0c;并调用之。

//带指定容量参数的构造函数public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}

3&#xff09;构造函数3——带指定容量大小和负载因子大小参数的构造函数

//带指定容量大小和负载因子大小参数的构造函数
public HashMap(int initialCapacity, float loadFactor) {//指定的容量大小不可以小于0,否则将抛出IllegalArgumentException异常if (initialCapacity <0)throw new IllegalArgumentException("Illegal initial capacity: " &#43;initialCapacity);//判定指定的容量大小是否大于HashMap的容量极限if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity &#61; MAXIMUM_CAPACITY;//指定的负载因子不可以小于0或为Null&#xff0c;否则将抛出IllegalArgumentException异常if (loadFactor <&#61; 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " &#43;loadFactor);//传入用户指定的负载因子 this.loadFactor &#61; loadFactor;//Map扩容的阀值大小//tableSizeFor方法用于查找到距离容量initialCapacity最近的2次幂值&#xff1b;this.threshold &#61; tableSizeFor(initialCapacity);
}

·


3.5&#xff09;tableSizeFor()方法&#xff1a; 

        在刚才的构造函数中&#xff0c;我们看到了tableSizeFor()这一个方法&#xff0c;将容量参数传进去&#xff0c;返回结果赋值给threshold 。

看源码&#xff1a;

static final int tableSizeFor(int cap) {int n &#61; cap - 1;n |&#61; n >>> 1;n |&#61; n >>> 2;n |&#61; n >>> 4;n |&#61; n >>> 8;n |&#61; n >>> 16;return (n <0) ? 1 : (n >&#61; MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n &#43; 1;}

        可以看出&#xff0c;其实代码很简单&#xff0c;就是对容量cap进行了几次无符号右移的操作&#xff0c;最后返回。

那么这个方法的目的和作用是什么呢&#xff1f; 

        这个方法的作用是&#xff1a;用于查找距离容量cap最近的2次幂值&#xff0c;比如&#xff1a;


  •         cap是5~7&#xff0c;那么该方法的返回结果就是8&#xff1b;
  •         cap是9~15&#xff0c;那么该方法的返回结果就是16&#xff1b;
  •         cap是17~31&#xff0c;那么该方法的返回结果就是32&#xff1b;
  •         以此类推...

·


4&#xff09;构造函数4——传入一个Map集合的构造函数&#xff1a;


        此构造方法主要实现了Map.putAll()方法。


//传入一个Map集合,将Map集合中元素Map.Entry全部添加进HashMap实例中
public HashMap(Map m) {this.loadFactor &#61; DEFAULT_LOAD_FACTOR;//此构造方法主要实现了Map.putAll()putMapEntries(m, false);
}

·

·


三&#xff0c;HashMap的初始化&#xff1a;


1&#xff09;HashMap无论如何初始化&#xff0c;其容量都为0&#xff1b;

        刚才我们讲到&#xff0c;HashMap的构造函数有四种&#xff0c;1、默认的无参构造函数&#xff1b;2、带指定容量参数的构造函数&#xff1b;3、带指定容量大小和负载因子大小参数的构造函数&#xff1b;4、传入一个Map集合的构造函数。

//无参的构造函数
public HashMap() {//加载负载因子&#xff1b;this.loadFactor &#61; DEFAULT_LOAD_FACTOR;
}
//传入容量参数的构造函数
public HashMap(int initialCapacity) {//调用下边的构造函数this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//传入容量参数和负载因子的构造函数
public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity <0)throw new IllegalArgumentException("Illegal initial capacity: " &#43;initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity &#61; MAXIMUM_CAPACITY;if (loadFactor <&#61; 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " &#43;loadFactor);this.loadFactor &#61; loadFactor;this.threshold &#61; tableSizeFor(initialCapacity);
}
//传入一个Map&#xff0c;核心是使用了putAll方法
public HashMap(Map m) {this.loadFactor &#61; DEFAULT_LOAD_FACTOR;putMapEntries(m, false);
}

        可以看出&#xff0c;其实用户在新建一个HashMap的时候&#xff0c;不管有没有传入容量参数&#xff0c;HashMap在初始化的时候其容量都是为0&#xff1b;

        比如说&#xff0c;如果用户没有传入容量参数&#xff0c;那么调用的是无参的构造函数进行初始化&#xff0c;此时容量为0&#xff1b;

        如果用户传入了容量参数&#xff0c;那么也只是将容量参数initialCapacity通过tableSizeFor方法找出距离该initialCapacity最近的2的幂次方数值&#xff0c;然后将该数值赋给了扩容阀值threshold&#xff1b;

为什么不管怎么初始化、用哪个构造函数&#xff0c;HashMap在初始化的时候其容量都是0呢&#xff1f;

        这是因为HashMap使用的懒加载机制&#xff0c;只有你第一次向HashMap中添加元素时&#xff0c;才进行第一次的容量设置&#xff0c;


查看put()方法的源码&#xff1a;

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

继续查看putVal()的源码&#xff1a;


        这里我们只分析关于触发初始化容量阀值和扩容的部分&#xff0c;put元素的详情请参见&#xff1a;HashMap的put方法与get方法_Morning sunshine的博客-CSDN博客


里边有两句源码是这样写的&#xff1a;

//触发初始化容量阀值&#xff1a;
if ((tab &#61; table) &#61;&#61; null || (n &#61; tab.length) &#61;&#61; 0)n &#61; (tab &#61; resize()).length;//...其他代码//触发扩容&#xff1a;
if (&#43;&#43;size > threshold)resize();

        意思是&#xff0c;如果此时entry数组为null或者entry数组的长度为0&#xff0c;也就是意味着这是第一次put元素&#xff0c;那么会调用resize()方法&#xff0c;

        如果entry数组的长度size大于容量阀值threshold了&#xff0c;也会去调用resize()方法。

        这个resize()方法是做什么的呢&#xff1f;就是用来初始化容量阀值或者扩容Map的一个方法

·


2&#xff09;看resize()方法如何初始化容量阀值&#xff1a;


final Node[] resize() {//在初始化的时候&#xff0c;entry数组oldTab为空&#xff0c;此时oldCap&#61;&#61;0Node[] oldTab &#61; table; int oldCap &#61; (oldTab &#61;&#61; null) ? 0 : oldTab.length; //如果用户在初始化的时候传入了容量参数&#xff0c;则threshold>0&#xff0c;如果没有传入容量参数&#xff0c;则threshold&#61;&#61;0&#xff1b;int oldThr &#61; threshold;int newCap, newThr &#61; 0;if (oldCap > 0) {if (oldCap >&#61; MAXIMUM_CAPACITY) {threshold &#61; Integer.MAX_VALUE;return oldTab;}else if ((newCap &#61; oldCap <<1) &#61; DEFAULT_INITIAL_CAPACITY)newThr &#61; oldThr <<1; // double threshold}//如果初始化的时候用户传入了容量参数&#xff0c;则此时oldThr大于0&#xff0c;那么将oldThr赋给newCapelse if (oldThr > 0)newCap &#61; oldThr; //如果初始化的时候用户什么都没传&#xff0c;那么此时oldCap和oldThr都&#61;&#61;0&#xff1a;则 else { //将默认容量大小DEFAULT_INITIAL_CAPACITY(16)赋给newCap&#xff1b;newCap &#61; DEFAULT_INITIAL_CAPACITY;//将默认负载因子0.75f*默认的容量大小DEFAULT_INITIAL_CAPACITY的计算结果&#xff0c;赋值给newThr&#xff1b;newThr &#61; (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}//如果初始化的时候用户传入了容量参数和负载因子或者只传入了容量参数&#xff0c;//那么意味着oldCap&#61;&#61;0、oldThr>0&#xff0c;那么上边的else里边是不会进入的&#xff0c;那么此时newThr仍然&#61;&#61;0&#xff1a;if (newThr &#61;&#61; 0) { //所以&#xff0c;将用户传入的容量参数*用户传入的负载因子loadFactor(也可能是默认的负载因子)&#xff0c;//将计算结果赋值给newThr&#xff1b;float ft &#61; (float)newCap * loadFactor; newThr &#61; (newCap [] newTab &#61; (Node[])new Node[newCap];table &#61; newTab;//...其他的扩容逻辑//返回entry数组&#xff1b;return newTab;
}

逻辑概括&#xff1a;



1、如果初始化的时候用户传入了容量参数和负载因子&#xff0c;或者只传入了容量参数&#xff0c;

        1.1、那么将 用户传入的容量参数 * 用户传入的负载因子loadFactor(也可能是默认的负载因子)&#xff0c;的计算结果&#xff0c;赋值给扩容阀值threshold

        1.2、将容量参数赋值给变量newCap&#xff1b;


2、如果初始化的时候用户什么都没传&#xff0c;

        2.1、那么将 默认负载因子0.75f * 默认的容量大小DEFAULT_INITIAL_CAPACITY&#xff0c;的计算结果&#xff0c;赋值给扩容阀值threshold

        2.2、将默认容量大小DEFAULT_INITIAL_CAPACITY(16)赋给newCap&#xff1b;


3、根据newCap创建一个Node数组即entry数组&#xff0c;

        大小为newCap&#xff1b;将该初始化好的entry数组返回出去。

        


        关于resize()方法里边的扩容逻辑&#xff0c;请参见下文&#xff1b;


·

·


四、Hashmap源码-负载因子

·        


1&#xff09;什么是负载因子&#xff1a;

static final float DEFAULT_LOAD_FACTOR &#61; 0.75f;final float loadFactor;public HashMap() {this.loadFactor &#61; DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

        负载因子在初始化Map的时候被赋值&#xff0c;要么被用户传的参数赋值&#xff0c;要么被默认的0.75f给赋值&#xff1b;

        默认的负载因子我们之前说过&#xff1a;0.75f&#xff0c;当然也可以自己设置&#xff0c;但 0.75 是最均衡的设置&#xff0c;没有特殊要求不要修改该值&#xff0c;

        负载因子过小&#xff0c;说明Node元素比较少&#xff0c;理论上能减少 hash 冲突&#xff0c;

        负载因子过大&#xff0c;可以节约空间。

·


2&#xff09;负载因子loadFactor&#xff0c;在哪里被赋值&#xff1a;

        在Map的构造函数中&#xff1a;

public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity <0)throw new IllegalArgumentException("Illegal initial capacity: " &#43;initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity &#61; MAXIMUM_CAPACITY;if (loadFactor <&#61; 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " &#43;loadFactor);this.loadFactor &#61; loadFactor;this.threshold &#61; tableSizeFor(initialCapacity);}

从代码中可以看到&#xff0c;

        如果我们设置的初始化容量小于0&#xff0c;将会抛出异常&#xff0c;

        如果加载因子小于0也会抛出异常

        同时&#xff0c;如果初始容量大于最大容量&#xff0c;则重新设置为最大容量

·


3&#xff09;tableSizeFor()方法是做什么的&#xff1a;

        我们看最后两行代码&#xff0c;首先&#xff0c;对负载因子进行赋值&#xff0c;这个没什么可说的。

        牛逼的是下面一行代码&#xff1a;this.threshold &#61; tableSizeFor(initialCapacity);

我们进入到该tableSizeFor方法查看&#xff1a; 

static final int tableSizeFor(int cap) {int n &#61; cap - 1;n |&#61; n >>> 1;n |&#61; n >>> 2;n |&#61; n >>> 4;n |&#61; n >>> 8;n |&#61; n >>> 16;return (n <0) ? 1 : (n >&#61; MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n &#43; 1;}

        可以看出&#xff0c;其实代码很简单&#xff0c;就是对容量cap进行了几次无符号右移的操作&#xff0c;最后返回。

那么这个方法的目的和作用是什么呢&#xff1f; 

        这个方法的作用是&#xff1a;用于查找距离容量initialCapacity最近的2次幂值&#xff0c;比如&#xff1a;


  •         cap是5~7&#xff0c;那么该方法的返回结果就是8&#xff1b;
  •         cap是9~15&#xff0c;那么该方法的返回结果就是16&#xff1b;
  •         cap是17~31&#xff0c;那么该方法的返回结果就是32&#xff1b;
  •         以此类推...

        我们仔细观察其算法的过程&#xff0c;可以说&#xff0c;任何一个int 数字&#xff0c;都能找到离他最近的 2 的幂次方数字&#xff08;且比他大&#xff09;。


·

·


五&#xff0c;resize()方法里边的扩容逻辑


1&#xff09;怎么计算的扩容阀值&#xff1a;

      扩容阀值threshold &#61; 负载因子loadFactor * 容量capacity

·


2&#xff09;什么时候触发扩容&#xff1a;

         当entry数组的长度到达了扩容的阀值threshold之后&#xff0c;就会调用resize()方法进行扩容&#xff1b;

·


3&#xff09; resize方法的两个作用&#xff1a;

        resize方法的两个作用是&#xff1a;用来初始化一个Map&#xff0c;或者对Map容量进行扩容

        初始化请参见上文&#xff1b;

        扩容时&#xff0c;因为我们使用二次幂展开&#xff0c;每个 bin 中的元素必须保持相同的索引&#xff0c;或者在新表中以二次幂的偏移量移动。
·


4&#xff09;源码解析&#xff1a;

//重新设置table大小/扩容 并返回扩容的Node数组即HashMap的最新数据
final Node[] resize() {Node[] oldTab &#61; table; //table赋予oldTab作为扩充前的table数据int oldCap &#61; (oldTab &#61;&#61; null) ? 0 : oldTab.length; int oldThr &#61; threshold;int newCap, newThr &#61; 0;if (oldCap > 0) {//判定数组是否已达到极限大小&#xff0c;若判定成功将不再扩容&#xff0c;直接将老表返回if (oldCap >&#61; MAXIMUM_CAPACITY) {threshold &#61; Integer.MAX_VALUE;return oldTab;}//若新表大小(oldCap*2)小于数组极限大小 并且 老表大于等于数组初始化大小else if ((newCap &#61; oldCap <<1) &#61; DEFAULT_INITIAL_CAPACITY)//旧数组大小oldThr 经二进制运算向左位移1个位置 即 oldThr*2当作新数组的大小newThr &#61; oldThr <<1; // double threshold}//若老表中下次扩容大小oldThr大于0else if (oldThr > 0)newCap &#61; oldThr; //将oldThr赋予控制新表大小的newCapelse { //若其他情况则将获取初始默认大小newCap &#61; DEFAULT_INITIAL_CAPACITY;newThr &#61; (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}//若新表的下表下一次扩容大小为0if (newThr &#61;&#61; 0) { float ft &#61; (float)newCap * loadFactor; //通过新表大小*负载因子获取newThr &#61; (newCap [] newTab &#61; (Node[])new Node[newCap];table &#61; newTab; //将当前表赋予tableif (oldTab !&#61; null) { //若oldTab中有值需要通过循环将oldTab中的值保存到新表中for (int j &#61; 0; j e;if ((e &#61; oldTab[j]) !&#61; null) {//获取老表中第j个元素 赋予eoldTab[j] &#61; null; //并将老表中的元素数据置Nullif (e.next &#61;&#61; null) //若此判定成立 则代表e的下面没有节点了newTab[e.hash & (newCap - 1)] &#61; e; //将e直接存于新表的指定位置else if (e instanceof TreeNode) //若e是TreeNode类型//分割树&#xff0c;将新表和旧表分割成两个树&#xff0c;并判断索引处节点的长度是否需要转换成红黑树放入新表存储((TreeNode)e).split(this, newTab, j, oldCap);else { // preserve orderNode loHead &#61; null, loTail &#61; null; //存储与旧索引的相同的节点Node hiHead &#61; null, hiTail &#61; null; //存储与新索引相同的节点Node next;//通过Do循环 获取新旧索引的节点do {next &#61; e.next;if ((e.hash & oldCap) &#61;&#61; 0) {if (loTail &#61;&#61; null)loHead &#61; e;elseloTail.next &#61; e;loTail &#61; e;}else {if (hiTail &#61;&#61; null)hiHead &#61; e;elsehiTail.next &#61; e;hiTail &#61; e;}} while ((e &#61; next) !&#61; null);//通过判定将旧数据和新数据存储到新表指定的位置if (loTail !&#61; null) {loTail.next &#61; null;newTab[j] &#61; loHead;}if (hiTail !&#61; null) {hiTail.next &#61; null;newTab[j &#43; oldCap] &#61; hiHead;}}}}}//返回新表return newTab;}

· 


5&#xff09;扩容逻辑步骤&#xff1a; 


  • 1.判定数组是否已达到极限大小&#xff0c;若判定成功将不再扩容&#xff0c;直接将老表返回&#xff1b;

  • 2.若新表大小(oldCap2)小于数组极限大小&老表大于等于数组初始化大小 判定成功则 旧数组大小oldThr 经二进制运算向左位移1个位置 即 oldThr2当作新数组的大小

    • 2.1. 若[2]的判定不成功&#xff0c;则继续判定 oldThr &#xff08;代表 老表的下一次扩容量&#xff09;大于0&#xff0c;若判定成功 则将oldThr赋给newCap作为新表的容量

    • 2.2 若 [2] 和[2.1]判定都失败,则走默认赋值 代表 表为初次创建

  • 3.确定下一次表的扩容量, 将新表赋予当前表

  • 4.通过for循环将老表中的值存入扩容后的新表中

    • 4.1 获取旧表中指定索引下的Node对象 赋予e 并将旧表中的索引位置数据置空

    • 4.2 若e的下面没有其他节点则将e直接赋到新表中的索引位置

    • 4.3 若e的类型为TreeNode红黑树类型

      ​ 4.3.1 分割树&#xff0c;将新表和旧表分割成两个树&#xff0c;并判断索引处节点的长度是否需要转换成红黑树放入新表存储

      ​ 4.3.2 通过Do循环 不断获取新旧索引的节点

      ​ 4.3.3 通过判定将旧数据和新数据存储到新表指定的位置


门限值等于&#xff08;负载因子&#xff09;x&#xff08;容量&#xff09;&#xff0c;如果构建 HashMap 的时候没有指定它们&#xff0c;那么就是依据相应的默认常量值。

门限通常是以倍数进行调整 &#xff08;newThr &#61; oldThr <<1&#xff09;&#xff0c;我前面提到&#xff0c;根据 putVal 中的逻辑&#xff0c;当元素个数超过门限大小时&#xff0c;则调整 Map 大小。

扩容后&#xff0c;需要将老的数组中的元素重新放置到新的数组&#xff0c;这是扩容的一个主要开销来源。


·

·


原文链接&#xff1a;深入理解 HashMap put 方法&#xff08;JDK 8逐行剖析&#xff09;_stateiso的博客-CSDN博客_hashmap的put方法实现原理



4&#xff09;源码解析2&#xff1a;

我们看看该方法实现&#xff1a;

final Node[] resize() {Node[] oldTab &#61; table;int oldCap &#61; (oldTab &#61;&#61; null) ? 0 : oldTab.length;int oldThr &#61; threshold;int newCap, newThr &#61; 0;// 如果老的容量大于0if (oldCap > 0) {// 如果容量大于容器最大值if (oldCap >&#61; MAXIMUM_CAPACITY) {// 阀值设为int最大值threshold &#61; Integer.MAX_VALUE;// 返回老的数组&#xff0c;不再扩充return oldTab;}// 如果老的容量*2 小于最大容量并且老的容量大于等于默认容量else if ((newCap &#61; oldCap <<1) &#61; DEFAULT_INITIAL_CAPACITY)// 新的阀值也再老的阀值基础上*2newThr &#61; oldThr <<1; // double threshold}// 如果老的阀值大于0else if (oldThr > 0) // initial capacity was placed in threshold// 新容量等于老阀值newCap &#61; oldThr;else { // 如果容量是0&#xff0c;阀值也是0&#xff0c;认为这是一个新的数组&#xff0c;使用默认的容量16和默认的阀值12 newCap &#61; DEFAULT_INITIAL_CAPACITY;newThr &#61; (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 如果新的阀值是0&#xff0c;重新计算阀值if (newThr &#61;&#61; 0) {// 使用新的容量 * 负载因子&#xff08;0.75&#xff09;float ft &#61; (float)newCap * loadFactor;// 如果新的容量小于最大容量 且 阀值小于最大 则新阀值等于刚刚计算的阀值&#xff0c;否则新阀值为 int 最大值newThr &#61; (newCap [] newTab &#61; (Node[])new Node[newCap];// 将新数组赋值给当前对象的数组属性table &#61; newTab;// 如果老的数组不是nullif (oldTab !&#61; null) {// 循环老数组for (int j &#61; 0; j e;// 如果老数组对应下标的值不为空if ((e &#61; oldTab[j]) !&#61; null) {// 设置为空oldTab[j] &#61; null;// 如果老数组没有链表if (e.next &#61;&#61; null)// 将该值散列到新数组中newTab[e.hash & (newCap - 1)] &#61; e;// 如果该节点是树else if (e instanceof TreeNode)// 调用红黑树 的split 方法&#xff0c;传入当前对象&#xff0c;新数组&#xff0c;当前下标&#xff0c;老数组的容量&#xff0c;目的是将树的数据重新散列到数组中((TreeNode)e).split(this, newTab, j, oldCap);else { // 如果既不是树&#xff0c;next 节点也不为空&#xff0c;则是链表&#xff0c;注意&#xff0c;这里将优化链表重新散列&#xff08;java 8 的改进&#xff09;// Java8 之前&#xff0c;这里曾是并发操作会出现环状链表的情况&#xff0c;但是Java8 优化了算法。此bug不再出现&#xff0c;但并发时仍然不建议HashMapNode loHead &#61; null, loTail &#61; null;Node hiHead &#61; null, hiTail &#61; null;Node next;do {next &#61; e.next;// 这里的判断需要引出一些东西&#xff1a;oldCap 假如是16&#xff0c;那么二进制为 10000&#xff0c;扩容变成 100000&#xff0c;也就是32.// 当旧的hash值 与运算 10000&#xff0c;结果是0的话&#xff0c;那么hash值的右起第五位肯定也是0&#xff0c;那么该于元素的下标位置也就不变。if ((e.hash & oldCap) &#61;&#61; 0) {// 第一次进来时给链头赋值if (loTail &#61;&#61; null)loHead &#61; e;else// 给链尾赋值loTail.next &#61; e;// 重置该变量loTail &#61; e;}// 如果不是0&#xff0c;那么就是1&#xff0c;也就是说&#xff0c;如果原始容量是16&#xff0c;那么该元素新的下标就是&#xff1a;原下标 &#43; 16&#xff08;10000b&#xff09;else {// 同上if (hiTail &#61;&#61; null)hiHead &#61; e;elsehiTail.next &#61; e;hiTail &#61; e;}} while ((e &#61; next) !&#61; null);// 理想情况下&#xff0c;可将原有的链表拆成2组&#xff0c;提高查询性能。if (loTail !&#61; null) {// 销毁实例&#xff0c;等待GC回收loTail.next &#61; null;// 置入bucket中newTab[j] &#61; loHead;}if (hiTail !&#61; null) {hiTail.next &#61; null;newTab[j &#43; oldCap] &#61; hiHead;}}}}}return newTab;}


5&#xff09;扩容逻辑步骤2&#xff1a;

        该方法可以说还是比较复杂的。初始的时候也是调用的这个方法&#xff0c;当链表数超过8的时候同时数组长度小于64的时候也是调用的这个方法。该方法步骤如下&#xff1a;

        1、判断entry数组的长度是否大于0&#xff0c;如果大于0&#xff0c;并且已经大于最大值MAXIMUM_CAPACITY&#xff0c;则设置阀值为 int 类型的最大值&#xff0c;并返回原数组&#xff0c;不再扩容&#xff1b;

        2、如果entry数组的长度乘以 2 小于最大容量MAXIMUM_CAPACITY&#xff0c;且大于等于16&#xff0c;则更新阀值、就是乘以2。

        3、如果老的阀值大于0&#xff0c;则新的容量等于老的阀值。

        4、如果老的阀值和容量都不大于0&#xff0c;则认为是一个新的数组&#xff0c;默认初始容量为16&#xff0c;阀值为 16 * 0.75f&#xff0c;也就是 12。

        5、如果&#xff0c;新的阀值还是0&#xff0c;那么就使用我们刚刚设置的容量&#xff08;HashMap 帮我们算的&#xff09;&#xff0c;通过乘以 0.75&#xff0c;得到一个阀值&#xff0c;然后判断算出的阀值是否合法&#xff1a;如果容量小于最大容量并且阀值小于最大容量&#xff0c;那么则使用该阀值&#xff0c;否则使用 int 最大值。

        6、将刚刚的阀值设置到当前Map实例的阀值属性中。

        7、将刚刚的entry数组设置到当前Map实例的entry数组属性中。

        8、如果老的entry数组不是null&#xff0c;则将原来entry数组中的值重新散列到新的entry数组中。如果是null&#xff0c;直接返回新entry数组。

那么&#xff0c;将老数组重新散列的过程到底是怎么样的呢&#xff1f;

·


6&#xff09;扩容时是如何重新散列元素的&#xff1a;

重新散列的代码&#xff1a;

// 如果老的数组不是null
if (oldTab !&#61; null) {// 循环老数组for (int j &#61; 0; j e;// 如果老数组对应下标的值不为空if ((e &#61; oldTab[j]) !&#61; null) {// 设置为空oldTab[j] &#61; null;// 如果老数组没有链表if (e.next &#61;&#61; null)// 将该值散列到新数组中newTab[e.hash & (newCap - 1)] &#61; e;// 如果该节点是树else if (e instanceof TreeNode)// 调用红黑树 的split 方法&#xff0c;传入当前对象&#xff0c;新数组&#xff0c;当前下标&#xff0c;老数组的容量&#xff0c;目的是将树的数据重新散列到数组中((TreeNode)e).split(this, newTab, j, oldCap);else { // 如果既不是树&#xff0c;next 节点也不为空&#xff0c;则是链表&#xff0c;注意&#xff0c;这里将优化链表重新散列&#xff08;java 8 的改进&#xff09;// Java8 之前&#xff0c;这里曾是并发操作会出现环状链表的情况&#xff0c;但是Java8 优化了算法。此bug不再出现&#xff0c;但并发时仍然不建议HashMapNode loHead &#61; null, loTail &#61; null;Node hiHead &#61; null, hiTail &#61; null;Node next;do {next &#61; e.next;// 这里的判断需要引出一些东西&#xff1a;oldCap 假如是16&#xff0c;那么二进制为 10000&#xff0c;扩容变成 100000&#xff0c;也就是32.// 当旧的hash值 与运算 10000&#xff0c;结果是0的话&#xff0c;那么hash值的右起第五位肯定也是0&#xff0c;那么该于元素的下标位置也就不变。if ((e.hash & oldCap) &#61;&#61; 0) {// 第一次进来时给链头赋值if (loTail &#61;&#61; null)loHead &#61; e;else// 给链尾赋值loTail.next &#61; e;// 重置该变量loTail &#61; e;}// 如果不是0&#xff0c;那么就是1&#xff0c;也就是说&#xff0c;如果原始容量是16&#xff0c;那么该元素新的下标就是&#xff1a;原下标 &#43; 16&#xff08;10000b&#xff09;else {// 同上if (hiTail &#61;&#61; null)hiHead &#61; e;elsehiTail.next &#61; e;hiTail &#61; e;}} while ((e &#61; next) !&#61; null);// 理想情况下&#xff0c;可将原有的链表拆成2组&#xff0c;提高查询性能。if (loTail !&#61; null) {// 销毁实例&#xff0c;等待GC回收loTail.next &#61; null;// 置入bucket中newTab[j] &#61; loHead;}if (hiTail !&#61; null) {hiTail.next &#61; null;newTab[j &#43; oldCap] &#61; hiHead;}}}}
}

这里楼主重新贴了上面的代码&#xff0c;因为这段代码比较重要。还是说一下该部分代码的步骤。

        1、首先循环老数组。下标从0开始&#xff0c;如果对应下标的值不是null&#xff0c;则判断该值有没有next 节点&#xff0c;也就是判断是否是链表。

        2、如果不是链表&#xff0c;则根据新的数组长度重新hash该元素。

        3、如果该节点是树&#xff0c;则调用红黑树的 split 方法&#xff0c;传入当前对象和新数组还有下标&#xff0c;该方法会重新计算红黑树中的每个hash值&#xff0c;如果重新计算后&#xff0c;树中元素的hash值不同&#xff0c;则重新散列到不同的下标中。达到拆分红黑树的目的&#xff0c;提高性能。具体如何拆分下面再说。

        4、之后的判断就是链表&#xff0c;在Java8中&#xff0c;该部分代码不是简单的将旧链表中的数据拷贝到新数组中的链表就完了&#xff0c;而是会对旧的链表进行重新 hash&#xff0c;如果 hash 得到的值和之前不同&#xff0c;则会从旧的链表中拆出&#xff0c;放到另一个下标中去&#xff0c;提高性能&#xff0c;刚刚的红黑树也是这么做的。

        这里的重新hash 不是使用的 [e.hash & (newCap - 1)] 方法&#xff0c;而是使用更加效率的方法&#xff0c;直接 hash 老的数组容量&#xff0c;就没有了减一的操作&#xff0c;可见 JDK 的作者为了性能可以说是无所不用其极了。

其实我们的注释已经写了&#xff0c;但是楼主还是再解释一遍吧&#xff1a;

仔细阅读下面这段话&#xff1a;

        oldCap 假如是16&#xff0c;那么二进制为 10000&#xff0c;扩容变成 100000&#xff0c;也就是32.

        当旧的hash值 与运算 10000&#xff0c;结果是0的话&#xff0c;那么hash值的右起第五位肯定也是0&#xff0c;那么该于元素的下标位置也就不变。

        但如果不是0是1的话&#xff0c;说明该hash值变化了&#xff0c;那么就需要对这个元素重新散列放置。

        那么应该放哪里呢&#xff1f;如果是16&#xff0c;那么最左边是1的话&#xff0c;说明hash值变大了16&#xff0c;那么只需要在原有的基础上加上16便好了。

        这段代码还有一个需要注意的地方&#xff1a;在JDK 7 中&#xff0c;这里的的代码是不同的&#xff0c;在并发情况下会链表会变成环状&#xff0c;形成死锁

        而JDK 8 已经修复了该问题&#xff0c;但是仍然不建议使用 HashMap 并发编程


总结

        截至到这里&#xff0c;我们的 HashMap 的 put 方法已经剖析完了&#xff0c;此次可以说收获不小&#xff1a;

        我们知道了&#xff0c;无论我们如何设置初始容量&#xff0c;HashMap 都会将我们改成2的幂次方&#xff0c;也就是说&#xff0c;HashMap 的容量百分之百是 2的幂次方&#xff0c;因为HashMap 太依赖他了。

        但是&#xff0c;请注意&#xff1a;如果我们预计插入7条数据&#xff0c;那么我们写入7&#xff0c;HashMap 会设置为 8&#xff0c;虽然是2的幂次方&#xff0c;但是&#xff0c;请注意&#xff0c;当我们放入第7条数据的时候&#xff0c;就会引起扩容&#xff0c;造成性能损失&#xff0c;所以&#xff0c;知晓了原理&#xff0c;我们以后在设置容量的时候还是自己算一下&#xff0c;比如放7条数据&#xff0c;我们还是都是设置成16&#xff0c;这样就不会扩容了。

        HashMap 的默认加载因子是 0.75&#xff0c;虽然可以修改&#xff0c;但是出于安全考虑&#xff0c;除非你经过大量测试&#xff0c;请不要修改此值&#xff0c;HashMap 使用此值基本是平衡了性能和空间的取舍。

        HashMap 扩容的时机是&#xff0c;容器中的元素数量 > 负载因此 * 容量&#xff0c;如果负载因子是0.75&#xff0c;容量是16&#xff0c;那么当容器中数量达到13 的时候就会扩容。还有&#xff0c;如果某个链表长度达到了8&#xff0c;并且容量小于64&#xff0c;则也会用扩容代替红黑树。

        HashMap 在 JDK 7 中并发扩容的时候是非常危险的&#xff0c;非常容易导致链表成环状。但 JDK 8 中已经修改了此bug。但还是不建议使用。强烈推荐并发容器 ConcurrentHashMap

        HashMap 扩容的时候&#xff0c;不管是链表还是红黑树&#xff0c;都会对这些数据进行重新的散列计算&#xff0c;然后缩短他们的长度&#xff0c;优化性能。

        在进行散列计算的时候&#xff0c;会进一步优化性能&#xff0c;减少减一的操作&#xff0c;直接使用& 运算。可谓神来之笔。

        总之&#xff0c;HashMap 中的优秀的设计思想值得我们去学习&#xff0c;最让楼主震惊的就是那个将任意一个数变成了2的幂次方的数&#xff0c;并且该数字很合理&#xff0c;说实话&#xff0c;如果让楼主写&#xff0c;楼主是写不出来的。

        所以&#xff0c;请努力吧&#xff0c;现在做不到&#xff0c;不代表以后做不到&#xff0c;通过学习优秀的源码&#xff0c;一定能够提高我们的编码能力。


 



推荐阅读
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 高质量SQL书写的30条建议
    本文提供了30条关于优化SQL的建议,包括避免使用select *,使用具体字段,以及使用limit 1等。这些建议是基于实际开发经验总结出来的,旨在帮助读者优化SQL查询。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • ALTERTABLE通过更改、添加、除去列和约束,或者通过启用或禁用约束和触发器来更改表的定义。语法ALTERTABLEtable{[ALTERCOLUMNcolu ... [详细]
author-avatar
你走之后你的美我如何收拾_686
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有