Java 容器之 HashMap 详解


前言

本文基于 jdk 1.8 分析 HashMap 源码,主要内容有

  • 基础知识简介
  • HashMap 的底层存储详解

在 jdk1.8 中,HashMap 使用了红黑树来优化,若要彻底理解 HashMap 的相关知识,可以先了解红黑树的相关内容,然后再阅读本文。


主要参数

本小节主要介绍 HashMap 的核心参数及其值设定原理。

  • loadFactor
    loadFactor 负载系数(也称负载因子),默认值为 0.75。该值决定了一个 HashMap 实例对象的存储容量将在何时进行扩容。举个简单但不是那么恰当的例子,如图 1 将 hash 表看成是一个装水的桶,在往桶中加入水,当水的体积到达桶的容量的 75% 时则需要更换更大的桶来装水。更换桶后需将小桶的水加入到大桶中
    在这里插入图片描述

    图 1

    那么 0.75 值是否是固定不可变?

    0.75 是 jdk 设定的默认值,定义该值是权衡时间复杂度和空间复杂度的平衡点而考虑的,该值可在创建 HashMap 实例时通过传入参数改变,其取值范围为 0 ~ 1。

    
        /**
         * The load factor used when none specified in constructor.
         */
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
    

    若实例化 HashMap 时未指定负载系数,将使用默认的值 0.75。负载因子越大,hash 表中可装的元素越多,空间利用率高,但带来的问题是 hash 冲突增加,由于 HashMap 中采用的是拉链法解决 hash 冲突,当冲突增加时,桶中的链表将增加,导致查询性能降低。负载因子越小,hash 表中可装的元素越少,虽减少了 hash 冲突,但需要频繁扩容导致空间利用率不高浪费资源。

    实际使用过程中,可依据业务需求适当更改,不过大部分情况下,使用默认值即可。

    为何一定是 0.75?

    注意看源码中给出的很关键的一个词—— Poisson distribution(泊松分布)。

    	 * Because TreeNodes are about twice the size of regular nodes, we
         * use them only when bins contain enough nodes to warrant use
         * (see TREEIFY_THRESHOLD). And when they become too small (due to
         * removal or resizing) they are converted back to plain bins.  In
         * usages with well-distributed user hashCodes, tree bins are
         * rarely used.  Ideally, under random hashCodes, the frequency of
         * nodes in bins follows a **Poisson distribution**
         * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
         * parameter of about 0.5 on average for the default resizing
         * threshold of 0.75, although with a large variance because of
         * resizing granularity. Ignoring variance, the expected
         * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
         * factorial(k)). The first values are:
         *
         * 0:    0.60653066
         * 1:    0.30326533
         * 2:    0.07581633
         * 3:    0.01263606
         * 4:    0.00157952
         * 5:    0.00015795
         * 6:    0.00001316
         * 7:    0.00000094
         * 8:    0.00000006
    

    在理想情况下,使用随机哈希码,节点出现的频率在hash桶中遵循泊松分布,同时给出了桶中元素个数和概率的对照表。从表中可以看到当桶中元素到达8个的时候,概率已经变得非常小,也就是说用 0.75 作为负载因子,每个碰撞位置的链表长度超过8个是几乎不可能的。

  • TREEIFY_THRESHOLD & MIN_TREEIFY_CAPACITY
    这两个参数定义了 hash 表的桶的链表转红黑树的阈值,取值分别为 8,64。只有当桶中的链表长度 >= 8 且 hash 表中元素的数量 > 64 时,才会将链表转化为红黑树。

    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
         treeifyBin(tab, hash);   // 链表长度大于 8 后,尝试转化为红黑树
    
    final void treeifyBin(Node<K,V>[] tab, int hash) {
         
       int n, index; Node<K,V> e;
       // 再次 hash 表的元素是否大于等于最小元素转化数量 64,不满足则继续扩容
       if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 
           resize();
    

    为何是 8、64 的取值?

    链表转化为红黑树是一个复杂的过程,且红黑树的数据结构 TreeNode 所需内存是链表 Node 的两倍,费时且费空间。深层次的原因还是基于泊松分布的概率计算而来,而不是拍脑门子决定。

  • UNTREEIFY_THRESHOLD
    该属性定义了有红黑树转化为列表的值,默认为 6。即当红黑树的节点数为 6 是,将桶的数据结构将由红黑树转化为链表。

    为何取值为 6?

在这里插入图片描述


存储详解

存储结构

在 jdk1.8 中,HashMap 存储数据使用的数据结构是数组 + 链表 / 红黑树。

  • 链表节点数据结构为
    static class Node<K,V> implements Map.Entry<K,V> {
         
            final int hash;    // hash 值
            final K key;    // map 中的 key
            V value;    // map 中的 value
            Node<K,V> next;  // 下一个 node
    }
    
  • 红黑树节点数据结构为
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
         
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值