HashMap原理及源码分析

HashMap 原理及源码分析

1. 存储结构

HashMap 内部是由 Node 类型的数组实现的。 Node 包含着键值对,内部有四个字段,从 next 字段我们可以看出, Node 是一个链表。即数组的每一个位置被当作一个桶,每个桶存储一个链表。HashMap 使用拉链法来解决冲突,同一个桶中存放 hashcode 与数组长度取模运算结果相同的 Node

HashMap原理及源码分析
static class Node implements Map.Entry {
    final int hash;
    final K key;
    V value;
    Node next;

    Node(int hash, K key, V value, Node next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry e = (Map.Entry)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

2. 索引计算

在 HashMap 中, node 在数组中的索引根据 key 的哈希值与数组长度取模运算得到。

如下为 HashMap 中 putVal 方法(put方法的实际执行过程),

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node[] tab; Node p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // ...

}

可以看到第6行中

i = (n - 1) & hash

其中 i 即为根据 key 计算得到的索引,n 为数组的大小。

这是因为在 HashMap 中, 数组的大小为 (2^n), 则其二进制形式具有如下特点:

令 x = 1 << 4,即 x 为 2 的 4 次方

x   : 0001 0000
x-1 : 0000 1111

令一个数 y 与 x – 1 取与

y       : 1101 1011
x-1     : 0000 1111
y&(x-1) : 0000 1011

这个性质和 y 对 x 取模效果是一样的

y       : 1101 1011
x       : 0001 0000
y%x     : 0000 1011

我们知道,位运算的代价比求模运算小的多,因此使用这种计算时用位运算能够带来更高的性能。

3. put 操作

HashMap 中 put 方法实际执行的是 putVal 方法。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

下面为 putVal 方法的实现,为了更好的理解 put 操作,源码中有关红黑树及扩容的部分,先使用省略号代替。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node[] tab; Node p; int n, i;
    // map 刚构造并且未添加元素时,数组为 null,需要进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 计算索引,若索引位置的桶为空,则将其放入桶中
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node e; K k;
        // 若桶的头节点的 key 与要插入的 key 相同,将其赋给 e(e 为需要更新的节点)
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // ...

        else {
            // 遍历桶中的链表
            for (int binCount = 0; ; ++binCount) {
                // 如果到达链表尾部,即 map 中不存在该 key 的节点,则将其添加到尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // ...

                    break;
                }
                // 如果在遍历过程中存在 key,则将其赋给 e
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // e 不为空,说明 map 中存在该 key,更新其 value
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //...

}

总结

put 操作主要步骤为:

  1. 根据 key 计算 node 在数组中的索引
  2. 若索引位置为空,则将 node 放入该索引位置
  3. 若不为空
  4. 遍历该索引位置的桶中元素,比较桶中元素 key 是否与该 key 相同,若相同则更新其 value
  5. 若桶中不存在该 key,则新建一个 node 放入桶中

4. 扩容

设 HashMap 中数组的长度为 n, 存储的键值对数量为 m, 在哈希函数满足均匀性的前提下,每个链表长度为 m / n。因此查找的时间复杂度为 O(m / n)。

为了减小查找效率,即减小 m / n, 则应该增大 n,即数组的长度。HashMap 使用动态扩容的方式来根据当前键值对的数量,来调整数组的大小,是得空间和时间效率都能得到保证。

和扩容相关的参数主要有:capatity、size、threshold 和 loadFactor

HashMap原理及源码分析

参数 含义 capacity table 数组的大小,默认为 16 size 键值对数量 threshold size 的临界值,当 size 大于 threshold 时就必须进行扩容操作 loadFactor 装载因子,默认为 0.75,table 能够使用的比例 threshold = (int) (capacity * loadFactor)

扩容操作在 putVal 中,有两种情况会触发

  • map 初始时,数组大小为 0,需要扩容
  • 当键值对数量超过临界值时,触发扩容
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node[] tab; Node p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length; /* 数组为 null,需要扩容 */
    // ...
    if (++size > threshold)
        resize(); /* size 超过临界值,触发扩容 */
    // ...

}

扩容主要分为两步

  1. 对数组容量进行扩容
  2. 扩容后需要原来数组的所有键插入到新的数组中
final Node[] resize() {
    Node[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 1. 进行扩容
    if (oldCap > 0) {
        // 若容量大于等于最大容量,则无法扩容,将临界值改为 int 的最大值,并返回(因为没有扩容无需重新插入键)
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 容量扩大到原来的两倍,并将临界值修改到原来的两倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1;
    }
    else if (oldThr > 0) // 若数组大小为 0,临界值不为 0,则数组初始大小为临界值(tableSizeFor 方法保证 oldThr 为 2 的 n 次方,下一节介绍)
        newCap = oldThr;
    else {  // 若数组大小为 0,临界值为 0,则初始大小为 16,初始临界值 = 0.75 * 16 = 12
        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;
    Node[] newTab = (Node[])new Node[newCap];
    table = newTab;
    // 2. 重新插入键
    if (oldTab != null) {
        // 遍历旧的数组
        for (int j = 0; j < oldCap; ++j) {
            Node 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)e).split(this, newTab, j, oldCap);
                else { // 遍历链表
                    Node loHead = null, loTail = null;
                    Node hiHead = null, hiTail = null;
                    Node next;
                    do {
                        next = e.next;
                        /**
                         *  举个例子
                         *  oldCap  : 0001 0000
                         *  newCap  : 0010 0000
                         *  newCap-1: 0001 1111
                         *  若哈希与旧数组大小与运算为 0,则说明
                         *  哈希与 newCap-1 运算时,与最高位1运算
                         *  的结果为 0,则索引位置位于 [0,oldCap)
                         *  若运算结果不为 0,则索引位置为 [oldCap, newCap)
                         *  又同一个桶中,与 oldCap-1 运算结果相同,
                         *  即同一个桶中的元素,计算的新索引要么为旧索引值,
                         *  要么为旧索引值+oldCap(与最高位1相与不为0)
                         *  则 loHead 为旧索引位置的头节点,hiHead 为
                         *  新索引位置的头节点
                        */
                        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;
}

5. 保证数组容量为 (2^n)

HashMap 的构造函数允许用户传入初始容量,并且可以不是 (2^n)。但是 HashMap 可以将其转为 (2^n) 。

先考虑如何求一个数的掩码,对于 1000 0000,它的掩码为 1111 1111。可以使用以下方法得到:

mask |= mask >> 1   1100 0000
mask |= mask >> 2   1111 0000
mask |= mask >> 4   1111 1111

则 mask + 1 是大于原始数字的最小的 2 的 n 次方

HashMap原理及源码分析

HashMap 通过 tableSizeFor 来保证数组容量为 2 的 n 次方

HashMap原理及源码分析

Original: https://www.cnblogs.com/Code-CHAN/p/15795234.html
Author: Code-CHAN
Title: HashMap原理及源码分析

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/713617/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • shopify主题模板startup修改

    shopify startup主题是一个很好的直接面向消费者DTC品牌的shopify模板,具有增强的推荐部分可定制性强,适合‎ ‎服装和配饰, 健康与美容, 家居与园艺‎,Dro…

    技术杂谈 2023年5月31日
    0105
  • list,map,set的区别

    list,map,set的区别 (首先假定小猪都是同一个细胞克隆出来的) List = 排成一长队的小猪 Map = 放在一个个,有房间号的屋子里面的一群小猪 Set = 一群小猪…

    技术杂谈 2023年5月31日
    071
  • Nightingale 监控报警平台

    Nightingale 从官方的介绍是企业版的prometheus,从功能上的确是很不错的,我们基本上可以实现基于ui 灵活的管理prometheus 的报警处理 参考架构 Vic…

    技术杂谈 2023年5月30日
    099
  • Python实现XMind测试用例快速转Excel用例

    转载请注明出处❤️ 作者:测试蔡坨坨 原文链接:caituotuo.top/c2d10f21.html 你好,我是测试蔡坨坨。 今天分享一个Python编写的小工具,实现XMind…

    技术杂谈 2023年7月11日
    094
  • Composer安装扩展,一直提示PHP版本不对,但实际版本是满足的

    问题: 最近用Laravel框架开发,因为开发一个功能,需要做图片比对。所以需要安装一个插件,于是使用composer安装,但是一直安装失败,提示PHP最低版本需要>=7.4…

    技术杂谈 2023年7月10日
    060
  • 三维数字沙盘+GIS = F3DGIS

    关键字:F3DGIS,数字沙盘,3D,GIS,电子沙盘 三维数字沙盘又称为虚拟沙盘、数字沙盘系统,是利用各种新技术,如:三维技术、地理遥控技术、虚拟现实技术、触控技术等,在计算机中…

    技术杂谈 2023年5月31日
    074
  • 009 Pycharm的使用(各种骚操作和快捷键)

    博客配套视频链接: https://space.bilibili.com/383551518?spm_id_from=333.1007.0.0 b 站直接看 配套 github 链…

    技术杂谈 2023年5月31日
    096
  • Maven配置私有仓库

    前言 当公司或个人具有自己独有的jar时,不想公开,一般就会放在自己的私有Maven仓库中,在项目中需要引用,此时就需要将公司私有仓库配置到maven当中,一般我们的maven配置…

    技术杂谈 2023年6月21日
    0100
  • gin protobuf客户端测试

    gin protobuf客户端测试 gin protobuf客户端测试 // clientGIN project main.go package main import ( &qu…

    技术杂谈 2023年5月30日
    074
  • Red Hat dhclient

    如果你是通过dhcp动态获取ip进行上网,我们一般情况下需要对/etc/sysconfig/network-scripts目录下对应的网卡配置进行修改,将BOOTPROTO改为dh…

    技术杂谈 2023年6月1日
    089
  • wasm示例 js canvas 动画示例

    3d迷宫移动:https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/A_basic_ray-caster C:\d…

    技术杂谈 2023年5月31日
    093
  • [教程] 【亲测可用】阿里云盘挂载变成你的电脑本地硬盘详细教程

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    技术杂谈 2023年5月31日
    0108
  • sf2gis@163.com

    3、安装python,我安装在了D:\Python25,环境变量设置PATH D:\Python25; 4、開始->程序->Microsoft Visual Studi…

    技术杂谈 2023年5月31日
    083
  • Rust学习入门

    高性能,内存利用率高,没有运行时和垃圾回收可靠 , 丰富的类型系统和所有权模型保证内存和线程安全,编译器可以消除各种错误生产力, 包管理器、构建工具一流, 多编辑器支持自动补齐和格…

    技术杂谈 2023年7月11日
    069
  • python学习

    ; ; 目录: 1、课程推荐以及书籍推荐 2、学习记录 2.1:无 1. 实践过程 廖雪峰的官方网站 2. 学习记录 2.1 无: posted @2022-02-12 19:44…

    技术杂谈 2023年6月21日
    0113
  • 建造者模式(创建型)

    建造者模式 介绍 建造者模式注重的是部件构建的过程,意在 通过一步一步地精确构造出一个复杂的对象。 可以将建造者模式理解为,假设我们有一个对象需要建立,这个对象是由多个组件(Com…

    技术杂谈 2023年6月21日
    095
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球