目录1. 如何查看源码2. 查看源码时需要注意的几个小细节3. HashMap 的无参构造4. HashMap 的带参构造(只给一个初始容量)5. HashMap的put方法源码解读6. HashMap的扩容解读7. HashMap可以存空值吗8. 总结概括(非常重要)1. 如何查看源码在学习 HashMap 源码之前我先来教各位如何在 IDEA 编辑器中查看Java源码这里我就当各位都是小白从零说起。我们打开我们的 IDEA如下图所示是我编写的一个类当我们想要查看某个类的源码时可以在键盘上点击 Ctrl 键 N 键此时IDEA 编辑器页面上就会出现如下所示的一个小搜索框我们就可以在搜索框中输入我们想要查看的类或者接口的源码如下图中所示我输入 String 搜索一下这里就会出现一堆关于 String 的相关类或接口当我们想查看时直接点击对应自己想要查看的类或接口即可跳转至源码我点击 String 跳转至 String 类的源码如下图所示然后我们就可以查看源码了这里还有一个小知识点我们在源码类中我们可以点击键盘上的 Ctrl 键 F12 键就会出现该源码类中所有的变量和方法如下图中所示各位学到了吗以后自己在看源码时也可以使用 Ctrl N 与 Ctrl F12 两个快捷组合键查看源码。2. 查看源码时需要注意的几个小细节刚才我们说到了如何查看源码这里我再说几个查看源码时需要注意的几个小细节如下图中所示我用红线标注出来的在前面都有一个小圆圈里面还有字母m这时英文单词 method 的缩写说明这是该源码类中的一个方法往下翻如下图小 f 开头的代表是英文单词 field 代表该类中的属性小 c 开头的则是class 的缩写指此类是我们查看的源码类中的一个内部类还有以 小 i 开头的是该类中的一个接口即 interface 如下所示每一条方法的最后是该方法的返回值类型3. HashMap 的无参构造如下图所示就是HashMap 的无参构造源码可以看到源码中无参构造方法很简单它仅仅赋值了一个loadFactor翻译过来就加载因子如下我找到了该变量为 0.75f这里我先简单解释0.75就是HashMap 的扩容时机也就是说当我们创建的 HashMap 集合中存储的元素达到了最大容量的0.75时就会触发扩容机制。4. HashMap 的带参构造(只给一个初始容量)如下图即为 HashMap 的带初始容量的构造器这里调用了一个方法我们点击 this 跟进查看找到 this 方法如下我们找到了真正的带参构造源码这里我一点一点解释首先这个方法传递了两个参数一个 initialCapacity 是初始容量loadFactory 是我们刚才我参构造中说到的扩容时机也为0.75(1)可以看到首先做了一个判断判断我们初始赋值的容量是否小于0如果是直接抛出异常(2)再判断我们给的初始容量大于最大容量吗这里我已经找到了最大容量如下图 1 30可能有一些小伙伴不太明白 1 30 是什么意思这里简单解释1 30 就是 2^30即2的30次方这一表示的就是 1 乘以 2^30如果是 表示除以 2 的多少次方听明白了吧(3) 这里再次对 loadFactory 做判断如果不动默认就是 0.75不用多说什么(4) 根据 ininialCapacity 调用另一个方法计算出变量 threshold的值5. HashMap的put方法源码解读如下所示即为HashMap的put方法源码这里一共有5个参数我简单说一下它们的含义hash(key)计算出键的哈希值key值value键对应的要存储的值onlyIfAbsent当遇到key一样时是否保留这里默认为覆盖而不是保留最后一个参数暂时不用管我们目前记住这四个参数一得以就可以了。这里它又调用了putVal方法我们点击查看这里的 table 需要记住它就是我们HashMap底层的那个数组的真实名称接着往下看putVal方法中我给了大量的注释尽可能解释的详细一些final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // 定义一个内部类 Node的数组定义一个内部类 Node的对象定义 ni两个变量 NodeK,V[] tab; NodeK,V p; int n, i; // 将table 对象的值赋值给定义的Node数组对象然后做判断 // 先判断 tab 等于空吗或者tab的长度为0吗无参构造初始容量就为空那么添加第一个元素时就会扩容 // 如果是这里调用了 resize()方法下面我展示到了 if ((tab table) null || (n tab.length) 0) n (tab resize()).length; // 这里对应该存放数据的位置坐一次判空操作看看是否已经存有元素了 if ((p tab[i (n - 1) hash]) null) // 如果为空则说明该位置还尚未添加任何元素就会执行这一行代码 // tab[i] 即为添加的元素应该存放在HashMap集合中的位置 tab[i] newNode(hash, key, value, null); // 上述不执行说明要存入元素的位置不为空进行下面的判断 else { // 既然不为空首先判断是否为链表定义一个内部类Node的对象e泛型对象 k NodeK,V e; K k; // 这里再次作判断判断 key 是否已经存在 // 如果相等则把值直接进行替换 if (p.hash hash ((k p.key) key || (key ! null key.equals(k)))) e p; // 上方链表的判断未执行则就是进入红黑树的判断了判断是否是红黑树中的一个节点 else if (p instanceof TreeNode) // 如果是则调用putTreeVal方法添加该节点到红黑树中 e ((TreeNodeK,V)p).putTreeVal(this, tab, hash, key, value); else { // 如果不是红黑树中的一个节点那个就把它挂在链表的下面用的是尾插法 for (int binCount 0; ; binCount) { if ((e p.next) null) { // 这里会判断数组的长度是否超过了64并且链表的长度是否尝过了8 // 如果都满足条件会把链表转换为红黑树 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; }我来简单描述一下如下假设我们定义了一个长度为16的HashMap下面在随机定义几个数据地址分别为 0x00110x00220x00330x00440x0055我们将它们依次存入HashMap1插入aaa时取对象地址为0x0011的aaa的key算出哈希值假设应该存入2的位置结合上述代码在插入时会先做判断判断2的位置是否为空现在明显是为空的直接存入如下所示2插入bbb时取对象地址为0x0022的bbb的key算出哈希值假设应该存入6的位置结合上述代码在插入时会先做判断判断6的位置是否为空现在明显是为空的直接存入如下所示3插入ccc时取对象地址为0x0033的ccc的key算出哈希值假设应该存入2的位置结合上述代码在插入时会先做判断判断2的位置是否为空现在是不为空的然后再使用equals方法判断已经存在的元素中是否有key与待存入的元素key相等这里明显不相等所以将ccc插入在aaa的下方形成链表之后还会判断长度是超过了8并且HashMap长度是否超过了64若超过需要将链表转化为红黑树这里没有所以不转化插入后结果如下图所示4插入ddd时取对象地址为0x0044的ddd的key算出哈希值假设也应该存入2的位置结合上述代码在插入时会先做判断判断2的位置是否为空现在是不为空的然后再使用equals方法判断已经存在的元素中是否有key与待存入的元素key相等这里明显不相等所以将ddd插入在ccc的下方形成链表之后还会判断长度是超过了8并且HashMap长度是否超过了64若超过需要将链表转化为红黑树这里没有所以不转化如下图所示(5)插入另一个ddd时取对象地址为0x0055的ddd的key算出哈希值假设也应该存入2的位置结合上述代码在插入时会先做判断判断2的位置是否为空现在是不为空的然后再使用equals方法判断已经存在的元素中是否有key与待存入的元素key相等经过判断发现已经存在了一个地址为0x0044的ddd值为444要新存入的0x0055的key也为ddd但值为555那么此时就会把0x0044的ddd所对应的值444覆盖为555注意这里不是把0x0044删除而是把0x0044对应的值改为了新的555。之后还会判断长度是超过了8并且HashMap长度是否超过了64若超过需要将链表转化为红黑树这里没有所以不转化结果如下图所示以上就是关于HashMap再添加元素时的流程我这里只举了简单的例子当元素多时满足了转换成红黑树的条件链表就转化为了红黑树在这里就不再列举了红黑树添加元素的过程可以参考我的另一篇文章这里把链接放在这里了有兴趣的可以观看多谢各位的支持。(1条消息) 数据结构——红黑树_程序猿ZhangSir的博客-CSDN博客https://blog.csdn.net/m0_70325779/article/details/131393598?spm1001.2014.3001.55016. HashMap的扩容解读刚才的 putVal方法中就调用了 resize 方法// resize()方法的源码 final NodeK,V[] resize() { // 这里先将内部定义好的table对象赋值给 oldTab NodeK,V[] oldTab table; // 这里做判断oldTab为空吗如果为空则赋值为0如果不为空则获取oldTab的长度并赋值给oldCap int oldCap (oldTab null) ? 0 : oldTab.length; // 这里将原来带参构造中计算出的threshold赋值给oldThr int oldThr threshold; // 定义newCapnewThr为0 int newCap, newThr 0; // 这里先做判断将上面得到的数组容量做判断判断是否大于0 if (oldCap 0) { // 如果大于0判断是否大于或等于最大容量若已经达到最大容量则返回原数组 if (oldCap MAXIMUM_CAPACITY) { threshold Integer.MAX_VALUE; return oldTab; } // 否则左移一个2进制位刚才说了左移乘以2的1次方就是扩容为原来的2倍 // 这里判断扩大为2倍后是否超过了最大容量 else if ((newCap oldCap 1) MAXIMUM_CAPACITY // 这里还要判断原来的容量是否大于等于默认初始容量必须两个条件都满足才会扩容之原来的2倍 oldCap DEFAULT_INITIAL_CAPACITY) newThr oldThr 1; // double threshold } else if (oldThr 0) // initial capacity was placed in threshold newCap oldThr; // 上述条件都不成立这里会把默认初始容量赋值给newCap为16 else { // zero initial threshold signifies using defaults newCap DEFAULT_INITIAL_CAPACITY; // 这里newThr代表实际应该扩容时数组的长度为 16 * 0.75 12 newThr (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 这里对newThr做判断若发生扩容则newThr的值应该发生改变改为新数组的0.75倍即新扩容数据再次扩容时的长度 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}) NodeK,V[] newTab (NodeK,V[])new Node[newCap]; table newTab; if (oldTab ! null) { for (int j 0; j oldCap; j) { NodeK,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) ((TreeNodeK,V)e).split(this, newTab, j, oldCap); else { // preserve order NodeK,V loHead null, loTail null; NodeK,V hiHead null, hiTail null; NodeK,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; }7. HashMap可以存空值吗有些时候面试官可能会问一些刁钻的问题就比如这个。问你HashMap是否可以存空值为什么能存空值我之前有次面试就被问到了这里先说结果HashMap可以存空值原因我们看源码可以得知HashMap在调用putVal方法之前参数列表中会先调用 hash 方法对键的哈希值做一次运算如上图所示我们可以看出在 hash 运算方法中它做了一次判空操作判断key null吗如果为空后面将它赋值为0即存入HashMap的0索引处。8. 总结概括(非常重要)通过上面对源码的解读我们大致可以得出以下几个结论1HashMap默认的初始容量为162HashMap的最大容量为 2^302的30次方3HashMap底层的数据名称为 table4HashMap集合中每个元素成为一个节点可能是链表节点也可能是红黑树节点。如果是链表节点存放的数据包括该节点的哈希值key值value值下一个结点的内存地址如果是红黑树节点存放的数据包括该节点的哈希值该节点的Key值该节点的value值该节点的颜色父结点地址值左子节点地址值右子节点地址值5在JDK8之前HashMap 底层数据结构为数组 链表在JDK8之后HashMap 底层数据结构为数据链表红黑树添加了红黑树这种数据结构提高了查找的效率6JDK8之前HashMap 添加元素用的是头插法也就是当两个元素哈希值相同产生哈希碰撞时新添加的元素会加在原来结点的头部JDK8之后HashMap 添加元素用的是尾插法新添加的元素会加在原来结点的尾部尾插法避免了头插法在多线程情况下可能产生环形链表的情况7HashMap 链表转换为红黑树必须同时满足以下两个条件一当前节点要插入的链表长度 8二是 HashMap 数组的长度 64如果只是当前链表满足数组长度不满足条件会进行数组扩容将链表的元素尽可能分散而不会将链表转化为红黑树如果同时满足了以上两个条件则会将当前待插入节点所在的链表转化为红黑树8HashMap 的默认加载因子为 0.75即当HashMap集合中的元素超过最大容量的0.75倍时就会出发扩容机制9HashMap 扩容每次扩容为原来的 2 倍9因为HashMap扩容需要涉及到数组的拷贝链表的拷贝以及红黑树的拷贝。非常消耗性能所以在开发过程中建议在定义时就选一个适当大小的容量作为初始容量这样可以避免后期频繁增删导致扩容而带来的性能消耗这种方法虽提高了程序的性能但同时也会造成一些内存的浪费尽管如此但开发通常讲究牺牲空间换时间所以还是建议在初始定义时就定义一个大小合适的容量
HashMap 底层源码深度解读
目录1. 如何查看源码2. 查看源码时需要注意的几个小细节3. HashMap 的无参构造4. HashMap 的带参构造(只给一个初始容量)5. HashMap的put方法源码解读6. HashMap的扩容解读7. HashMap可以存空值吗8. 总结概括(非常重要)1. 如何查看源码在学习 HashMap 源码之前我先来教各位如何在 IDEA 编辑器中查看Java源码这里我就当各位都是小白从零说起。我们打开我们的 IDEA如下图所示是我编写的一个类当我们想要查看某个类的源码时可以在键盘上点击 Ctrl 键 N 键此时IDEA 编辑器页面上就会出现如下所示的一个小搜索框我们就可以在搜索框中输入我们想要查看的类或者接口的源码如下图中所示我输入 String 搜索一下这里就会出现一堆关于 String 的相关类或接口当我们想查看时直接点击对应自己想要查看的类或接口即可跳转至源码我点击 String 跳转至 String 类的源码如下图所示然后我们就可以查看源码了这里还有一个小知识点我们在源码类中我们可以点击键盘上的 Ctrl 键 F12 键就会出现该源码类中所有的变量和方法如下图中所示各位学到了吗以后自己在看源码时也可以使用 Ctrl N 与 Ctrl F12 两个快捷组合键查看源码。2. 查看源码时需要注意的几个小细节刚才我们说到了如何查看源码这里我再说几个查看源码时需要注意的几个小细节如下图中所示我用红线标注出来的在前面都有一个小圆圈里面还有字母m这时英文单词 method 的缩写说明这是该源码类中的一个方法往下翻如下图小 f 开头的代表是英文单词 field 代表该类中的属性小 c 开头的则是class 的缩写指此类是我们查看的源码类中的一个内部类还有以 小 i 开头的是该类中的一个接口即 interface 如下所示每一条方法的最后是该方法的返回值类型3. HashMap 的无参构造如下图所示就是HashMap 的无参构造源码可以看到源码中无参构造方法很简单它仅仅赋值了一个loadFactor翻译过来就加载因子如下我找到了该变量为 0.75f这里我先简单解释0.75就是HashMap 的扩容时机也就是说当我们创建的 HashMap 集合中存储的元素达到了最大容量的0.75时就会触发扩容机制。4. HashMap 的带参构造(只给一个初始容量)如下图即为 HashMap 的带初始容量的构造器这里调用了一个方法我们点击 this 跟进查看找到 this 方法如下我们找到了真正的带参构造源码这里我一点一点解释首先这个方法传递了两个参数一个 initialCapacity 是初始容量loadFactory 是我们刚才我参构造中说到的扩容时机也为0.75(1)可以看到首先做了一个判断判断我们初始赋值的容量是否小于0如果是直接抛出异常(2)再判断我们给的初始容量大于最大容量吗这里我已经找到了最大容量如下图 1 30可能有一些小伙伴不太明白 1 30 是什么意思这里简单解释1 30 就是 2^30即2的30次方这一表示的就是 1 乘以 2^30如果是 表示除以 2 的多少次方听明白了吧(3) 这里再次对 loadFactory 做判断如果不动默认就是 0.75不用多说什么(4) 根据 ininialCapacity 调用另一个方法计算出变量 threshold的值5. HashMap的put方法源码解读如下所示即为HashMap的put方法源码这里一共有5个参数我简单说一下它们的含义hash(key)计算出键的哈希值key值value键对应的要存储的值onlyIfAbsent当遇到key一样时是否保留这里默认为覆盖而不是保留最后一个参数暂时不用管我们目前记住这四个参数一得以就可以了。这里它又调用了putVal方法我们点击查看这里的 table 需要记住它就是我们HashMap底层的那个数组的真实名称接着往下看putVal方法中我给了大量的注释尽可能解释的详细一些final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // 定义一个内部类 Node的数组定义一个内部类 Node的对象定义 ni两个变量 NodeK,V[] tab; NodeK,V p; int n, i; // 将table 对象的值赋值给定义的Node数组对象然后做判断 // 先判断 tab 等于空吗或者tab的长度为0吗无参构造初始容量就为空那么添加第一个元素时就会扩容 // 如果是这里调用了 resize()方法下面我展示到了 if ((tab table) null || (n tab.length) 0) n (tab resize()).length; // 这里对应该存放数据的位置坐一次判空操作看看是否已经存有元素了 if ((p tab[i (n - 1) hash]) null) // 如果为空则说明该位置还尚未添加任何元素就会执行这一行代码 // tab[i] 即为添加的元素应该存放在HashMap集合中的位置 tab[i] newNode(hash, key, value, null); // 上述不执行说明要存入元素的位置不为空进行下面的判断 else { // 既然不为空首先判断是否为链表定义一个内部类Node的对象e泛型对象 k NodeK,V e; K k; // 这里再次作判断判断 key 是否已经存在 // 如果相等则把值直接进行替换 if (p.hash hash ((k p.key) key || (key ! null key.equals(k)))) e p; // 上方链表的判断未执行则就是进入红黑树的判断了判断是否是红黑树中的一个节点 else if (p instanceof TreeNode) // 如果是则调用putTreeVal方法添加该节点到红黑树中 e ((TreeNodeK,V)p).putTreeVal(this, tab, hash, key, value); else { // 如果不是红黑树中的一个节点那个就把它挂在链表的下面用的是尾插法 for (int binCount 0; ; binCount) { if ((e p.next) null) { // 这里会判断数组的长度是否超过了64并且链表的长度是否尝过了8 // 如果都满足条件会把链表转换为红黑树 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; }我来简单描述一下如下假设我们定义了一个长度为16的HashMap下面在随机定义几个数据地址分别为 0x00110x00220x00330x00440x0055我们将它们依次存入HashMap1插入aaa时取对象地址为0x0011的aaa的key算出哈希值假设应该存入2的位置结合上述代码在插入时会先做判断判断2的位置是否为空现在明显是为空的直接存入如下所示2插入bbb时取对象地址为0x0022的bbb的key算出哈希值假设应该存入6的位置结合上述代码在插入时会先做判断判断6的位置是否为空现在明显是为空的直接存入如下所示3插入ccc时取对象地址为0x0033的ccc的key算出哈希值假设应该存入2的位置结合上述代码在插入时会先做判断判断2的位置是否为空现在是不为空的然后再使用equals方法判断已经存在的元素中是否有key与待存入的元素key相等这里明显不相等所以将ccc插入在aaa的下方形成链表之后还会判断长度是超过了8并且HashMap长度是否超过了64若超过需要将链表转化为红黑树这里没有所以不转化插入后结果如下图所示4插入ddd时取对象地址为0x0044的ddd的key算出哈希值假设也应该存入2的位置结合上述代码在插入时会先做判断判断2的位置是否为空现在是不为空的然后再使用equals方法判断已经存在的元素中是否有key与待存入的元素key相等这里明显不相等所以将ddd插入在ccc的下方形成链表之后还会判断长度是超过了8并且HashMap长度是否超过了64若超过需要将链表转化为红黑树这里没有所以不转化如下图所示(5)插入另一个ddd时取对象地址为0x0055的ddd的key算出哈希值假设也应该存入2的位置结合上述代码在插入时会先做判断判断2的位置是否为空现在是不为空的然后再使用equals方法判断已经存在的元素中是否有key与待存入的元素key相等经过判断发现已经存在了一个地址为0x0044的ddd值为444要新存入的0x0055的key也为ddd但值为555那么此时就会把0x0044的ddd所对应的值444覆盖为555注意这里不是把0x0044删除而是把0x0044对应的值改为了新的555。之后还会判断长度是超过了8并且HashMap长度是否超过了64若超过需要将链表转化为红黑树这里没有所以不转化结果如下图所示以上就是关于HashMap再添加元素时的流程我这里只举了简单的例子当元素多时满足了转换成红黑树的条件链表就转化为了红黑树在这里就不再列举了红黑树添加元素的过程可以参考我的另一篇文章这里把链接放在这里了有兴趣的可以观看多谢各位的支持。(1条消息) 数据结构——红黑树_程序猿ZhangSir的博客-CSDN博客https://blog.csdn.net/m0_70325779/article/details/131393598?spm1001.2014.3001.55016. HashMap的扩容解读刚才的 putVal方法中就调用了 resize 方法// resize()方法的源码 final NodeK,V[] resize() { // 这里先将内部定义好的table对象赋值给 oldTab NodeK,V[] oldTab table; // 这里做判断oldTab为空吗如果为空则赋值为0如果不为空则获取oldTab的长度并赋值给oldCap int oldCap (oldTab null) ? 0 : oldTab.length; // 这里将原来带参构造中计算出的threshold赋值给oldThr int oldThr threshold; // 定义newCapnewThr为0 int newCap, newThr 0; // 这里先做判断将上面得到的数组容量做判断判断是否大于0 if (oldCap 0) { // 如果大于0判断是否大于或等于最大容量若已经达到最大容量则返回原数组 if (oldCap MAXIMUM_CAPACITY) { threshold Integer.MAX_VALUE; return oldTab; } // 否则左移一个2进制位刚才说了左移乘以2的1次方就是扩容为原来的2倍 // 这里判断扩大为2倍后是否超过了最大容量 else if ((newCap oldCap 1) MAXIMUM_CAPACITY // 这里还要判断原来的容量是否大于等于默认初始容量必须两个条件都满足才会扩容之原来的2倍 oldCap DEFAULT_INITIAL_CAPACITY) newThr oldThr 1; // double threshold } else if (oldThr 0) // initial capacity was placed in threshold newCap oldThr; // 上述条件都不成立这里会把默认初始容量赋值给newCap为16 else { // zero initial threshold signifies using defaults newCap DEFAULT_INITIAL_CAPACITY; // 这里newThr代表实际应该扩容时数组的长度为 16 * 0.75 12 newThr (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 这里对newThr做判断若发生扩容则newThr的值应该发生改变改为新数组的0.75倍即新扩容数据再次扩容时的长度 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}) NodeK,V[] newTab (NodeK,V[])new Node[newCap]; table newTab; if (oldTab ! null) { for (int j 0; j oldCap; j) { NodeK,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) ((TreeNodeK,V)e).split(this, newTab, j, oldCap); else { // preserve order NodeK,V loHead null, loTail null; NodeK,V hiHead null, hiTail null; NodeK,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; }7. HashMap可以存空值吗有些时候面试官可能会问一些刁钻的问题就比如这个。问你HashMap是否可以存空值为什么能存空值我之前有次面试就被问到了这里先说结果HashMap可以存空值原因我们看源码可以得知HashMap在调用putVal方法之前参数列表中会先调用 hash 方法对键的哈希值做一次运算如上图所示我们可以看出在 hash 运算方法中它做了一次判空操作判断key null吗如果为空后面将它赋值为0即存入HashMap的0索引处。8. 总结概括(非常重要)通过上面对源码的解读我们大致可以得出以下几个结论1HashMap默认的初始容量为162HashMap的最大容量为 2^302的30次方3HashMap底层的数据名称为 table4HashMap集合中每个元素成为一个节点可能是链表节点也可能是红黑树节点。如果是链表节点存放的数据包括该节点的哈希值key值value值下一个结点的内存地址如果是红黑树节点存放的数据包括该节点的哈希值该节点的Key值该节点的value值该节点的颜色父结点地址值左子节点地址值右子节点地址值5在JDK8之前HashMap 底层数据结构为数组 链表在JDK8之后HashMap 底层数据结构为数据链表红黑树添加了红黑树这种数据结构提高了查找的效率6JDK8之前HashMap 添加元素用的是头插法也就是当两个元素哈希值相同产生哈希碰撞时新添加的元素会加在原来结点的头部JDK8之后HashMap 添加元素用的是尾插法新添加的元素会加在原来结点的尾部尾插法避免了头插法在多线程情况下可能产生环形链表的情况7HashMap 链表转换为红黑树必须同时满足以下两个条件一当前节点要插入的链表长度 8二是 HashMap 数组的长度 64如果只是当前链表满足数组长度不满足条件会进行数组扩容将链表的元素尽可能分散而不会将链表转化为红黑树如果同时满足了以上两个条件则会将当前待插入节点所在的链表转化为红黑树8HashMap 的默认加载因子为 0.75即当HashMap集合中的元素超过最大容量的0.75倍时就会出发扩容机制9HashMap 扩容每次扩容为原来的 2 倍9因为HashMap扩容需要涉及到数组的拷贝链表的拷贝以及红黑树的拷贝。非常消耗性能所以在开发过程中建议在定义时就选一个适当大小的容量作为初始容量这样可以避免后期频繁增删导致扩容而带来的性能消耗这种方法虽提高了程序的性能但同时也会造成一些内存的浪费尽管如此但开发通常讲究牺牲空间换时间所以还是建议在初始定义时就定义一个大小合适的容量