文章目录

  • 为什么扩容?
  • 什么时候扩容?
  • 如何扩容?

今天在和同时讨论HashMap的时候,提到了扩容和冲哈希的事情,然后我发现大家都是一种半懂不懂的状态。于是回去做了一番功课,写下这篇文章。

HashMap的扩容,又被很多人叫rehash、重哈希,我本人是很反对这个叫法的,事实上HashMap扩容的时候,Node中存储的Key的hash值并没有发生变化,只是Node的位置发生了变化。

首先说为什么需要扩容?这个问题好像有点简单,不就是因为容量满了就需要库容了吗?这种思想对于链表来说是没有问题的,但是对于HashMap来说,并不是因为这个原因,而是HashMap认为性能不够好时。

原因我简单说下,当然关于哈希表我就不再过多说了,还不懂的同学赶紧百度。

为什么扩容?

我们知道理论上哈希表的读时间复杂度是O(1),但是没有一种哈希方法能保证绝对的哈希均匀,为了解决哈希冲突又往往采用链地址法解决,那这样时间复杂度愈发偏离O(1)了,此时进行扩容,其实是让哈希表分散的更均匀,解决性能不够好的问题。

关于JDK1.8中的HashMap结构,在这里我就不多说了,哈希表+链表,长度达到一定时链表转换成红黑树(这时候是不是得有面试官出来问,长度多长才转红黑树啊?),为了让小伙伴们看的更直观,我这里偷一张图上来:

什么时候扩容?

那么什么时候需要扩容?答案也很简单:1.初始化后放入元素时 2.达到阈值时

  1. 创建对象以后,HashMap并不是立即初始化table,而是在第一次放入元素时,才会初始化table,这很HashMap节省内存得一种机制,而table的初始化其实是resize方法实现的。

  2. 达到阈值时,这个就比较有意思,所谓阈值,就是HashMapthreshold这个属性,阈值的计算方式很简单,基本上就是capacity(table容量) * loadFactor(负载因子),这里我觉得capacity应该称为理论容量,是因为正常情况下达到阈值就扩容了,达到阈值时HashMap认为哈希冲突的次数会不能接受,因此需要扩容。

因为这里我是以JDK1.8源码作为样本分析的,如果我没记错的话,JDK1.7中还存在rehash方法,但是JDK1.8中已经改名叫resize方法了,那我们就不管JDK1.7中是如何实现扩容,直接上JDK1.8源码。

如何扩容?

先来看扩容函数的前半部分:

final Node<K,V>[] resize() {// 扩容前原本的tableNode<K,V>[] oldTab = table;// 这里进行判断,区分尚未初始化的情况 int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) { // 非初始化情况/** 当原本的capacity已经超过最大值以后* HashMap选择不再扩容,然后threshold置为最大值*/if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}/** 这种是常见的的扩容情况,table容量会扩大两倍* 同时HashMap的阈值也跟着扩大两倍*/else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // 2倍阈值}else if (oldThr > 0) // 指定大小n的初始化情况下,table容量取>n的最小2倍数newCap = oldThr;else {               // 不指定初始化大小,table容量取默认值16newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) { // 指定大小初始化情况下,阈值 = table容量 * 负载因子(默认0.75)float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}// 最后确定阈值threshold = newThr;待续......
}

resize()方法的前半部分主要是对于新阈值和新容量的确定,这里有三种情况:

  1. 初始化已完成的正常扩容逻辑:table容量和阈值都扩大2倍
  2. 指定大小的初始化逻辑:算出大于等于指定初始化容量的最小2的倍数,作为table容量
  3. 未指定大小的初始化逻辑:默认table容量16,阈值为16 * 0.75 = 12

关于第二种逻辑,简单来说就是,指定初始化值为3,那么table容量就是4,如果指定初始化大小是10,那么table容量就是16,如果是2的次幂,就直接作为table容量。

再看resize方法的后半段:

final Node<K,V>[] resize() {......接上文@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) { // 遍历原本的tableNode<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null; // 为了GCif (e.next == null)// 如果table上没有链表的情况下,直接转移到对应位置// 转移到的位置就是get方法中取的下标位置,tab[(n - 1) & hash]newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)// 如果是红黑树,就进入红黑树的拆分逻辑,这里不展开来说((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else {// 原本槽内的一个链表,会被拆分成两个两个链表Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;// 如果entry的哈希值高位为0,会被拆分到lo链表if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {// 如果entry的哈希值高位为1,会被拆分到hi链表                            if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;// 高位是0,因此lo链表不需要移位,// hash & (newCap-1)和hash & (oldCap-1)获得下标位置一样newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;// 高位是1,hi链表移位到 j+oldCap位置,//  j + oldCap相当于高位补1,直接移到这个位置newTab[j + oldCap] = hiHead;}}}}}return newTab;}

这里为了能说的更通俗易懂一些,我举个简单的例子:

首先假设原本有几个key,他们的的hash值为:

key hash值 下标 hash & (length-1)
key1 00000101 00000101 = 5
key2 00010101 00000101 = 5
key3 00100101 00000101 = 5
key4 00110101 00000101 = 5

而假设原本table容量 oldCap = 16;

他们在table中存储的下标位置都是:

hash & (table.length-1) = 0101 & 0111

那么这几个key都会存储在table[5]位置,如下图 :

当进行扩容时,容量扩大一倍也就是newCap = 32,此时table.length -1 = 31

hash & (table.length - 1) 就会出现两种情况:

  1. key1 和 key3 的下标还是5 (0000 0101)
  2. key2 和 key4 的下标变为了 21 (0001 0101)
key hash值 下标 hash & (length-1)
key1 00000101 00000101 = 5
key2 00010101 00001101 = 21
key3 00100101 00000101 = 5
key4 00110101 00001101 = 21

到这应该能明白,resize方法里的lo列表和hi列表是什么意思了,其实就是看key高一位的哈希值是1还是0,来决定是放到哪个队列里。 移位后的HashMap如下图:

这里HashMap非常精妙的实现了扩容,没有重新计算对象的哈希值,甚至连下标的重新计算也只需要进行一位相与的计算(hash高位 & newCap-1 )。

HashMap扩容流程相关推荐

  1. 聊一聊不同技术栈中hashmap扩容机制

    前言 hash简介 作为后端开发,说HashMap是我们最经常接触到的数据结构都不为过,而HashMap如其名最主要依赖的算法就是hash散列算法来存储和读取数据.         以关键码值K为自变 ...

  2. HashMap/HashMap存储/HashMap扩容

    HashMap Java 集合,也称作容器,主要是由两大接口 (Interface)派生出来的:Collection 和 Map. Map集合体系: Map集合特点: (1) 键值对存储(key-va ...

  3. Btrace详细指南(JDK7,监控HashMap扩容)

    背景     JAVA中如何排查疑难杂症,如何动态获取应用信息,我们有BTrace! PS:集团有大杀器arthas,这里我们先从最原始最广泛的BTrace开始,后面可以玩玩Greys(开源,强于BT ...

  4. hashmap扩容 面试_HashMap面试,看完这一篇就够了(上)

    以下HashMap源码的解析都是基于java8来讲解的. HashMap的结构是数组加链表的形式(jdk7中也是),在java8中引入了红黑树,由于红黑树的时间复杂度是O(log n),引入红黑树是为 ...

  5. hashmap扩容机制_图文并茂:HashMap经典详解!

    点击上方 Java后端,选择 设为星标 优质文章,及时送达 代码中的注解多看几遍,其中HashMap的扩容机制是要必懂知识!结合图片一起理解! 什么是 HashMap? HashMap 是基于哈希表的 ...

  6. hashmap扩容线程安全问题_HashMap在1.7 1.8中的线程安全问题

    HashMap的线程不安全主要体现在下面两个方面: 在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况. 在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况. ? ? 常被问 ...

  7. hashmap扩容机制_图文并茂,HashMap经典详解!

    Java面试笔试面经.Java技术每天学习一点 公众号Java面试 关注我不迷路 作者:feigeswjtu 来源:https://github.com/feigeswjtu/java-basics ...

  8. hashmap扩容_面试官问:HashMap在并发情况下为什么造成死循环?一脸懵

    这个问题是在面试时常问的几个问题,一般在问这个问题之前会问Hashmap和HashTable的区别?面试者一般会回答:hashtable是线程安全的,hashmap是线程不安全的. 那么面试官就会紧接 ...

  9. HashMap 扩容 加载因子

    HashMap 扩容 加载因子 最近在看HashMap源码,对于扩容因子=0.75感到很费解,为什么在用了75%的容量的时候就要进行扩容呢?数组中明明还有25%的空间没有使用.为什么不等到数组几乎满了 ...

最新文章

  1. vim中设置python代码缩进为4个空格
  2. 设python中有模块m_Python 模块
  3. 如何:在Maven项目(JUnit,Mockito,Hamcrest,AssertJ)中测试依赖项
  4. 理解group by
  5. 软件开发沉思录--ThoughtWorks文集
  6. 王思聪又双被限制消费了!
  7. 2017IEC计算机第二次作业
  8. 【案例】保健品行业如何优化供应链管理?APS系统来帮忙
  9. 【数据结构】树状数组笔记
  10. Android 图形驱动初始化(二十三)
  11. Ubuntu 手动更新firefox的flash插件
  12. 模2加法,模2减法,模2除法
  13. 【最新】网站下载工具,整站下载工具汇总
  14. 在计算机领域黑箱,计算机模拟电学黑箱
  15. 专访剑桥大学校长作者:柴静
  16. [Qt]QLabel的显示圆形
  17. All in!马斯克出价430亿美元收购Twitter全部股份,还有B计划
  18. ZOOMIT的使用方法
  19. paper—基于 GCN 的安卓恶意软件检测模型
  20. 树以及二叉树的常用性质以及遍历

热门文章

  1. 什么牌子的运动耳机好,蓝牙运动耳机推荐
  2. JavaScript判断是否为空对象的几种方法
  3. 战神Z7无线网络WLAN无法打开(开关打开后又自动关闭)
  4. Graph Decipher: A transparent dual-attention graph neural network 图解密器:一种透明的双注意图神经网络,用于理解节点分类的消息传递机制
  5. c++ 树状数组三种方法
  6. Windows10,夜间模式失效?
  7. 关于生活垃圾分类,可以使用垃圾分类小程序进行辅助识别
  8. WPF使用“阿里矢量图”简单实现LED数字显示
  9. 从苏宁电器到卡巴斯基第27篇:难忘的三年硕士时光 V
  10. 秋招Android面试总结:美团、携程、百度、腾讯、长银58