JDK8中的HashMap初始化和扩容机制详解

一、HashMap初始化方法

HashMap() 不带参数,默认初始化大小为16,加载因子为0.75;

HashMap(int initialCapacity) 指定初始化大小;

HashMap(int initialCapacity, float loadFactor) 指定初始化大小和加载因子大小;

HashMap(Map<? extends K,? extends V> m) 用现有的一个map来构造HashMap。

二、分析初始化过程

1、初始化代码测试用例

Map<String, String> map = new HashMap<>(3);
map.put("id", "1");
map.put("name", "riemann");
map.put("sex", "male");

2、初始化过程

public HashMap(int initialCapacity, float loadFactor) {
 // 初始化大小小于0,抛出异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    // 初始大小最大为默认最大值
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 加载因子要在0到1之间
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    // threshold是根据当前的初始化大小和加载因子算出来的边界大小,
    // 当桶中的键值对超过这个大小就进行扩容
    this.threshold = tableSizeFor(initialCapacity);
}

此时:loadFactor = 0.75 默认值

// 这个方法返回大于输入参数且最接近的2的整数次幂的数
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;
}

此时:threshold = 4

三、分析扩容过程

1、第一次执行put操作后

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果存储元素的table为空,则进行必要字段的初始化
    if ((tab = table) == null || (n = tab.length) == 0)
    	// 获取长度
        n = (tab = resize()).length;
    // 如果根据hash值获取的结点为空,则新建一个结点
    // 此处 & 代替了 % (除法散列法进行散列)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 这里的p结点是根据hash值算出来对应在数组中的元素
    else {
        Node<K,V> e; K k;
        // 如果新插入的结点和table中p结点的hash值,key值相同的话
        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);
                    // 链表长度大于8时,将链表转红黑树
                    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
                p = e;
            }
        }
        // 如果存在这个映射就覆盖
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // 判断是否允许覆盖,并且value是否为空
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 回调以允许LinkedHashMap后置操作
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 更改操作次数
    ++modCount;
    // 大于临界值
    if (++size > threshold)
    	// 将数组大小设置为原来的2倍,并将原先的数组中的元素放到新数组中
        // 因为有链表,红黑树之类,因此还要调整他们
        resize();
    // 回调以允许LinkedHashMap后置操作
    afterNodeInsertion(evict);
    return null;
}

2、第一put会进行resize()操作:

// 初始化或者扩容之后元素调整
final Node<K,V>[] resize() {
	// 获取旧元素数组的各种信息
    Node<K,V>[] oldTab = table;
    // 长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 扩容的临界值
    int oldThr = threshold;
    // 定义新数组的长度及扩容的临界值
    int newCap, newThr = 0;
    // 如果原table不为空
    if (oldCap > 0) {
    	// 如果数组长度达到最大值,则修改临界值为Integer.MAX_VALUE
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 下面就是扩容操作(2倍)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // threshold也变为二倍
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // threshold为0,则使用默认值
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 如果临界值还为0,则设置临界值
    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;
}

四、小结

第一次put后:threshold = newCap * loadFactor = oldThr * loadFactor = 4 * 0.75 = 3

第二次put后:++size = 3,不进行扩容

第三次put后:++size = 4,进行扩容

oldCap = oldTab.length = 3
newcap = oldCap << 1 = 6
threshold = newThr = newCap * loadFactor = 6 * 0.75 = 4

结论:设置初始化容量n,初始化threshold = 大于n数且最接近的2的整数次幂的数 * 负载因子

JDK8中的HashMap深入理解

一、首先看一下HashMap的数据结构(数组+链表/红黑树),如下图:

1、红黑树特性(缺一不可):

(1)、每个节点要么是红色要么是黑色。

(2)、根节点是黑色。

(3)、所有叶子节点都是黑色(叶子节点为NIL或者NULL节点)。

(4)、不存在两个连续的红色节点。

(5)、任意节点(包含跟节点)到其叶子节点的所有路径都包含相同数目的黑色节点。

2、为什么HashMap中使用红黑树而不使用AVL树呢?

红黑树被称为弱AVL树,牺牲了严格的高度平衡的优越条件为代价(红黑树左右子树的高度差不超过一倍即可)使其能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作;此外,由于它的设计,任何不平衡都会在三次旋转之内解决。因为HashMap的使用场景中插入和删除操作是非常频繁的,所以在HashMap中使用了红黑树。

3、红黑树RBT与平衡二叉树AVL比较:

(1)、红黑树和AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。

(2)、红黑树和AVL树的区别在于它使用颜色来标识节点的高度,它所追求的是局部平衡而不是AVL树中的非常严格的平衡。

(3)、AVL 树比红黑树更加平衡,但AVL树在插入和删除的时候也会存在大量的旋转操作。所以当你的应用涉及到频繁的插入和删除操作,切记放弃AVL树,选择性能更好的红黑树;当然,如果你的应用中涉及的插入和删除操作并不频繁,而是查找操作相对更频繁,那么就优先选择 AVL 树进行实现。

二、HashMap元素插入过程及一些参数的详解

1、首先,需要了解HashMap源码中几个重要的参数:

DEFAULT_INITIAL_CAPACITY:默认初始化大小

MAXIMUM_CAPACITY:最大容量

DEFAULT_LOAD_FACTOR:默认的负载因子

TREEIFY_THRESHOLD:链表转化为红黑树的阈值(包含)

UNTREEIFY_THRESHOLD:红黑树转化为链表的阈值(包含)

MIN_TREEIFY_CAPACITY:当数组大小小于该值时,不进行链表向红黑树的转化,而是进行扩容

2、HashMap存储元素过程:

(1)图中刚开始有计算 key 的 hash 值的设计?

拿到 key 的 hashCode,并将 hashCode 的高16位和 hashCode 进行异或(XOR)运算,得到最终的 hash 值。

(2)为什么要将 hashCode 的高16位参与运算?

主要是为了在 table 的长度较小的时候,让高位也参与运算,并且不会有太大的开销。

(3)为什么链表转红黑树的阈值是8?

我们平时在进行方案设计时,必须考虑的两个很重要的因素是:时间和空间。对于 HashMap 也是同样的道理,简单来说,阈值为8是在时间和空间上权衡的结果。红黑树节点大小约为链表节点的2倍,在节点太少时,红黑树的查找性能优势并不明显,付出2倍空间的代价不值得。理想情况下,使用随机的哈希码,节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的公式计算,链表中节点个数为8时的概率为0.00000006,这个概率足够低了,并且到8个节点时,红黑树的性能优势也会开始展现出来,因此8是一个较合理的数字。

(4)HashMap 的默认初始容量是多少?HashMap 的容量有什么限制吗?

默认初始容量是16。HashMap 的容量必须是2的N次方,HashMap 会根据我们传入的容量计算一个大于等于该容量的最小的2的N次方,例如传 9,容量为16。

(5)为什么HashMap 的容量必须是 2 的 N 次方?

计算索引位置的公式为:(n - 1) & hash,当 n 为2的N 次方时,n - 1为低位全是 1 的值,此时任何值跟 n - 1 进行 &运算的结果为该值的低 N 位,达到了和取模同样的效果,实现了均匀分布。实际上,这个设计就是基于公式:x mod 2^n = x & (2^n - 1),因为 &运算比 mod 具有更高的效率。当 n 不为 2 的 N 次方时,hash 冲突的概率明显增大。

(6)为什么HashMap的负载因子默认为0.75?

在HashMap的类注释上有如图一段解释:大致意思是说负载因子是0.75的时候,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 为什么JDK8中HashMap依然会死循环

    JDK8中HashMap依然会死循环! 是否你听说过JDK8之后HashMap已经解决的扩容死循环的问题,虽然HashMap依然说线程不安全,但是不会造成服务器load飙升的问题. 然而事实并非如此.少年可曾了解一种红黑树成环的场景,=v= 今日在查看监控时候发现,某一台机器load飙升 感觉问题不对劲,ssh大法登陆机器,top,top -Hp,jstack,jmap四连击保存下来堆栈,cpu使用最高的线程,内存信息准备分析. 首先查看使用最耗费cpu的线程堆栈信息 cat stack | g

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

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

  • HashMap底层实现原理详解

    一.快速入门 示例:有一定基础的小伙伴们可以选择性的跳过该步骤 HashMap是Java程序员使用频率最高的用于映射键值对(key和value)处理的数据类型.随着JDK版本的跟新,JDK1.8对HashMap底层的实现进行了优化,列入引入红黑树的数据结构和扩容的优化等.本文结合JDK1.7和JDK1.8的区别,深入探讨HashMap的数据结构实现和功能原理. Java为数据结构中的映射定义了一个接口java.uti.Map,此接口主要有四个常用的实现类,分别是HashMap,LinkedHas

  • java中hashmap容量的初始化实现

    HashMap使用HashMap(int initialCapacity)对集合进行初始化. 在默认的情况下,HashMap的容量是16.但是如果用户通过构造函数指定了一个数字作为容量,那么Hash会选择大于该数字的第一个2的幂作为容量.比如如果指定了3,则容量是4:如果指定了7,则容量是8:如果指定了9,则容量是16. 为什么要设置HashMap的初始化容量 在<阿里巴巴Java开发手册>中,有一条开发建议是建议我们设置HashMap的初始化容量. 下面我们通过具体的代码来了解下为什么会这么

  • JDK8中的HashMap初始化和扩容机制详解

    一.HashMap初始化方法 HashMap() 不带参数,默认初始化大小为16,加载因子为0.75: HashMap(int initialCapacity) 指定初始化大小: HashMap(int initialCapacity, float loadFactor) 指定初始化大小和加载因子大小: HashMap(Map<? extends K,? extends V> m) 用现有的一个map来构造HashMap. 二.分析初始化过程 1.初始化代码测试用例 Map<String

  • JDK8中新增的原子性操作类LongAdder详解

    前言 本文主要给大家介绍了关于JDK8新增的原子性操作类LongAdder的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍: LongAdder简单介绍 LongAdder类似于AtomicLong是原子性递增或者递减类,AtomicLong已经通过CAS提供了非阻塞的原子性操作,相比使用阻塞算法的同步器来说性能已经很好了,但是JDK开发组并不满足,因为在非常高的并发请求下AtomicLong的性能不能让他们接受,虽然AtomicLong使用CAS但是CAS失败后还是通过

  • JavaWeb中web.xml初始化加载顺序详解

    需求说明 做项目时,为了省事,起初把初始化的配置都放在每个类中 static加载,初始化配置一多,就想把它给整理一下,这里使用servlet中的init方法初始化. web.xml说明 首先了解下web.xml中元素的加载顺序: 启动web项目后,web容器首先回去找web.xml文件,读取这个文件 容器会创建一个 ServletContext ( servlet 上下文),整个 web 项目的所有部分都将共享这个上下文 容器将 转换为键值对,并交给 servletContext 容器创建 中的

  • java中类加载与双亲委派机制详解

    目录 类加载是什么 类加载器 双亲委派机制 BootStrapClassLoader ExtClassLoader AppClassLoader 为什么使用双亲委派机制 全盘负责委托机制 自定义类加载器 打破双亲委派机制 类加载是什么 把磁盘中的java文件加载到内存中的过程叫做类加载 当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM. 有如下 User 类 package dc.dccmmtop; public Class User { publi

  • C++11中异常处理机制详解

    目录 一.异常的引入 二.C++异常的关键字 三.异常的抛出与处理规则 四.异常缺陷的处理 五.自定义异常体系 六.异常规范 七.异常安全 八.异常的优缺点 1.优点 2.缺点 一.异常的引入 传统的C语言处理异常的方式有两种: 1.终止程序:使用assert断言语句,如果发生内存错误等,比如内存泄漏或者除0错误,都会直接终止程序. 2.返回错误码:通过错误码判断发生的异常的类型是什么,如系统的很多库的接口程序通过把错误码放到errno中,表示错误. 在实际中的C语言程序基本都是通过返回错误码的

  • Java中的反射机制详解

    Java中的反射机制详解 反射,当时经常听他们说,自己也看过一些资料,也可能在设计模式中使用过,但是感觉对它没有一个较深入的了解,这次重新学习了一下,感觉还行吧! 一,先看一下反射的概念: 主要是指程序可以访问,检测和修改它本身状态或行为的一种能力,并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义. 反射是Java中一种强大的工具,能够使我们很方便的创建灵活的代码,这些代码可以再运行时装配,无需在组件之间进行源代码链接.但是反射使用不当会成本很高! 看概念很晕的,继续往下

  • Android中的binder机制详解

    前言 Binder做为Android中核心机制,对于理解Android系统是必不可少的,关于binder的文章也有很多,但是每次看总感觉看的不是很懂,到底什么才是binder机制?为什么要使用binder机制?binder机制又是怎样运行的呢?这些问题只是了解binder机制是不够的,需要从Android的整体系统出发来分析,在我找了很多资料后,真正的弄懂了binder机制,相信看完这篇文章大家也可以弄懂binder机制. 1.Binder是什么? 要理解binder,先要知道IPC,Inter

  • Java并发中的Fork/Join 框架机制详解

    什么是 Fork/Join 框架 Fork/Join 框架是一种在 JDk 7 引入的线程池,用于并行执行把一个大任务拆成多个小任务并行执行,最终汇总每个小任务结果得到大任务结果的特殊任务.通过其命名也很容易看出框架主要分为 Fork 和 Join 两个阶段,第一阶段 Fork 是把一个大任务拆分为多个子任务并行的执行,第二阶段 Join 是合并这些子任务的所有执行结果,最后得到大任务的结果. 这里不难发现其执行主要流程:首先判断一个任务是否足够小,如果任务足够小,则直接计算,否则,就拆分成几个

  • Java多线程高并发中的Fork/Join框架机制详解

    1.Fork/Join框架简介 Fork/Join 它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出.Fork/Join 框架要完成两件事情: Fork:把一个复杂任务进行分拆,大事化小 :把一个复杂任务进行分拆,大事化小 Join:把分拆任务的结果进行合并 在 Java 的 Fork/Join 框架中,使用两个类完成上述操作: ForkJoinTask: 我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务.该类提供了

  • C++中函数匹配机制详解

    首先,编译器会确定候选函数然后确定可行函数可行函数,再从可行函数中进一步挑选 候选函数:重载函数集中的函数 可行函数:可以调用的函数 最后进行寻找最佳匹配 有以下几种规则 1.该函数的每个实参的匹配都不劣于其他可行函数 2.至少有一个实参的匹配优于其他可行函数的匹配 3.满足上面两种要求的函数有且只有一个 如果上面三个要求都没满足,则出现二义性 一些演示 各有一个精确匹配的实参,编译器报错,不满足条件3 error void func(int a,int b) { cout << "

随机推荐