基于hashmap 的扩容和树形化全面分析

 更新时间:2021年6月11日 10:00  点击:1911

一、树形化

//链表转红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
//红黑树转链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
/**
*最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表 (即 将链表 转换成红黑树)
*否则,若桶内元素太多时,则直接扩容,而不是树形化
*为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
**/
static final int MIN_TREEIFY_CAPACITY = 64;

第一个和第二个变量没有什么问题,关键是第三个:是表示只有在数组长度大于64的时候,才能树形化列表吗?

实际上,这两个变量是应用于不同场景的。

链表长度大于8的时候就会调用treeifyBin方法转化为红黑树,但是在treeifyBin方法内部却有一个判断,当只有数组长度大于64的时候,才会进行树形化,否则就只是resize扩容。

为什么呢?

因为链表过长而数组过短,会经常发生hash碰撞,这个时候树形化其实是治标不治本,因为引起链表过长的根本原因是数组过短。执行树形化之前,会先检查数组长度,如果长度小于 64,则对数组进行扩容,而不是进行树形化。

所以发生扩容的时候是在两种情况下

超过阈值

链表长度超过8,但是数值长度不足64

二、扩容机制

hashmap内部创建过程

构造器(只是初始化一下参数,也就代表着只有添加数据的时候才会构建数组和链表)—调用put方法—put方法会调用resize方法(在数组为空或者超过阈值的时候,put方法调用resize方法)

hashmap是如何扩容的

1.hashmap中阈值threshold的设定

刚开始,阈值设定为空

当未声明的hashmap的大小的时候,阈值设定就是默认大小16*默认负载因子0.75=12

当声明hashmap的大小的时候,会先调用一个函数把阈值设定为刚刚大于设定值的2的次方(比如说设定的大小是1000,那阈值就是1024),然后在resize方法中,先把阈值赋给容量大小,然后在把容量大小*0.75在赋值给阈值。

代码如下:

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;

2.数据转移

当数组为null的时候,会创建新的数组

当数组不为空,会把容量和阈值均*2,并创建一个容量为之前二倍的数组,然后把原有数组的数据都转移到新数组。

假设扩容前的 table 大小为 2 的 N 次方,元素的 table 索引为其 hash 值的后 N 位确定

扩容后的 table 大小即为 2 的 N+1 次方,则其中元素的 table 索引为其 hash 值的后 N+1 位确定,比原来多了一位

转移数据不在跟1.7一样重新计算hash值(计算hash值耗时巨大),只需要看索引中新增的是bit位是1还是0,

若为0则在新数组中与原来位置一样,

若为1则在新 原位置+oldCap 即可。

三、容量计算公式

扩容是一个特别耗性能的操作,所以当程序员在使用 HashMap 的时候,估算 map 的大小,初始化的时候给一个大致的数值,避免 map 进行频繁的扩容。

HashMap 的容量计算公式 :size/0.75 +1 。

原理就是保证,阈值(数组长度*0.75)>实际容量

HashMap的最大容量为什么是2的30次方(1左移30)?

在阅读hashmap的源码过程中,我看到了关于hashmap最大容量的限制,并产生了一丝疑问。

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

为啥最大容量是 1 << 30?

探究过程1 – 为什么是30

首先是 << 这个操作符必须要理解,在一般情况下 1 << x 等于 2^x。这是左移操作符,对二进制进行左移。

来看1 << 30。它代表将1左移30位,也就是0010...0

来看这样一段代码:

public static void main(String[] args){
        for (int i = 30; i <= 33; i++) {
            System.out.println("1 << "+ i +" = "+(1 << i));
        }
        System.out.println("1 << -1 = " + (1 << -1));
}

输出结果为:

1 << 30 = 1073741824
1 << 31 = -2147483648
1 << 32 = 1
1 << 33 = 2
1 << -1 = -2147483648

结果分析:

  • int类型是32位整型,占4个字节。
  • Java的原始类型里没有无符号类型。 -->所以首位是符号位 正数为0,负数为1
  • java中存放的是补码,1左移31位的为 16进制的0x80000000代表的是-2147483648–>所以最大只能是30

探究过程2 – 为什么是 1 << 30

探究完1相信大家对 为什么是30有一点点了解。那为什么是 1 << 30,而不是0x7fffffff即Integer.MAX_VALUE

我们首先看代码的注释

 /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

翻译一下大概就是:如果构造函数传入的值大于该数 ,那么替换成该数。

ok,我们看看构造函数的调用:

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

其中这一句:

if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;

看到这有很有疑问了,如果我要存的数目大于 MAXIMUM_CAPACITY,你还把我的容量缩小成 MAXIMUM_CAPACITY???

别急继续看:在resize()方法中有一句:

if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
}

在这里我们可以看到其实 hashmap的“最大容量“是Integer.MAX_VALUE;

总结

MAXIMUM_CAPACITY作为一个2的幂方中最大值,这个值的作用涉及的比较广。其中有一点比较重要的是在hashmap中容量会确保是 2的k次方,即使你传入的初始容量不是 2的k次方,tableSizeFor()方法也会将你的容量置为 2的k次方。这时候MAX_VALUE就代表了最大的容量值。

另外还有一点就是threshold,如果对hashmap有一点了解的人都会知道threshold = 初始容量 * 加载因子。也就是扩容的 门槛。相当于实际使用的容量。而扩容都是翻倍的扩容。那么当容量到达MAXIMUM_CAPACITY,这时候再扩容就是 1 << 31 整型溢出。

所以Integer.MAX_VALUE作为最终的容量,但是是一个threshold的身份。以上为个人经验,希望能给大家一个参考,也希望大家多多支持猪先飞。

[!--infotagslink--]

相关文章

  • java中JSONObject转换为HashMap(方法+main方法调用实例)

    这篇文章主要介绍了java中JSONObject转换为HashMap(方法+main方法调用实例),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-14
  • Golang Map实现赋值和扩容的示例代码

    这篇文章主要介绍了Golang Map实现赋值和扩容的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-05-11
  • hashMap扩容时应该注意这些死循环问题

    今天给大家带来的是关于Java的相关知识,文章围绕着hashMap扩容时的死循环问题展开,文中有非常详细的介绍及代码示例,需要的朋友可以参考下...2021-06-10
  • Java中HashMap集合的常用方法详解

    本篇文章给大家带来的内容是关于Java中HashMap集合的常用方法详解,有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。下面我们就来学习一下吧...2021-11-04
  • Java那点儿事之Map集合不为人知的秘密有哪些

    Map用于保存具有映射关系的数据,Map集合里保存着两组值,一组用于保存Map的key,另一组保存着Map的value,和查字典类似,通过key找到对应的value,通过页数找到对应的信息。用学生类来说,key相当于学号,value对应name,age,sex等信息。用这种对应关系方便查找...2021-10-21
  • 解析ConcurrentHashMap: 红黑树的代理类(TreeBin)

    ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment的结构和HashMap类似,是一种数组和链表结构,今天给大家普及java面试常见问题---ConcurrentHashMap知识,一起看看吧...2021-06-11
  • 手机扩容神器,金士顿高速TF卡速度赶超U盘!

    随着手机成为我们生活中的重要设备,我们对它的依赖性越来越大!这个小小的设备成为了我们的拍照利器、追剧神器、游戏利器,带给我们欢乐的同时,手机容量也成为了我们日常使用的瓶颈,不过好在安卓系的大部分手机目前都支持外接扩容,所以一张高品质的TF卡就成为了我们扩容神器。...2016-09-12
  • Java中HashMap里面key为null存放到哪

    这篇文章主要介绍了Java中HashMap里面key为null存放到哪,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-02-04
  • Java中ConcurrentHashMap是如何实现线程安全

    ConcurrentHashMap是一个哈希表,支持检索的全并发和更新的高预期并发。本文主要介绍了Java中ConcurrentHashMap是如何实现线程安全,感兴趣的可以了解一下...2021-11-03
  • 深入理解Java中的HashMap

    HashMap是Java程序员使用频率最高的用于映射(键值对)处理的数据类型。随着JDK(Java Developmet Kit)版本的更新,JDK1.8对HashMap底层的实现进行了优化,例如引入红黑树的数据结构和扩容的优化等。本文将深入探讨HashMap的结构实现和功能原理...2021-06-11
  • HashMap在JDK7与JDK8中的实现过程解析

    这几天学习了HashMap的底层实现,但是发现好几个版本的,代码不一,很多文章都是旧版本JDK1.6.JDK1.7的。现在我来分析下JDK7与JDK8中HashMap的实现过程...2021-09-18
  • java中Hashmap的get方法使用

    这篇文章主要介绍了java中Hashmap的get方法使用,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-09-13
  • Java中使用HashMap改进查找性能的步骤

    这篇文章主要介绍了Java中使用HashMap改进查找性能的步骤,帮助大家更好的理解和使用Java,感兴趣的朋友可以了解下...2021-02-07
  • 基于hashmap 的扩容和树形化全面分析

    这篇文章主要介绍了hashmap 的扩容和树形化的使用,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-06-11
  • Java面试题之HashMap 的 hash 方法原理是什么

    那天,小二去蔚来面试,面试官老王一上来就问他:HashMap 的 hash 方法的原理是什么?当时就把裸面的小二给蚌埠住了,这篇文章将详细解答该题目...2021-11-05
  • 谈谈Hashmap的容量为什么是2的幂次问题

    这篇文章主要介绍了谈谈Hashmap的容量为什么是2的幂次问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-09-24
  • Java多线程高并发中解决ArrayList与HashSet和HashMap不安全的方案

    ArrayList实现了可变大小的数组。它允许所有元素,包括null。ArrayList没有同步,HashMap和Hashtable类似,不同之处在于HashMap是非同步的,并且允许null,关于HashSet有一件事应该牢记,即就条目数和容量之和来讲,迭代是线性的,接下来让我们详细来了解吧...2021-11-16
  • JAVA中哈希表HashMap的深入学习

    这篇文章主要介绍了哈希表HashMap的深入学习,哈希表是一种非常重要的数据结构,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,本文会对java集合框架中HashMap的实现原理进行讲解。感兴趣的话可以一起来学习...2020-07-13
  • 解决StringBuffer和StringBuilder的扩容问题

    这篇文章主要介绍了解决StringBuffer和StringBuilder的扩容问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-07-14
  • java编程进阶小白也能手写HashMap代码

    这篇文章是一篇java小白进阶篇本文教大家手写一个HashMap实现的示例代码,有需要的朋友可以借鉴参考下,希望对大家能够有所进益,祝大家早日升职加薪...2021-10-15