Java重点源码回顾——HashMap1.7

news2024/9/28 23:30:05

1. 概述

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

HashMap在我们的日常使用中非常多,所以今天来阅读下它的源码,了解它具体的设计思想,能够帮助我们扩宽视野。

HashMap两个主要的版本是1.7和1.8,都是线程不安全的。1.8版本是在1.7版本的基础上引入了红黑树等优化,提升整体的效率。今天我们先来简单了解下HashMap1.7版本的源码。它的主要特点总结如下:
在这里插入图片描述

在HashMap1.7中,并没有引入红黑树。所以采用的方式还是拉链法。
在这里插入图片描述

2. 成员变量

因为HashMap1.7是采用拉链法存储数据的,所以需要将key-value键值对封装成一个节点类,如下所示:

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;  // 存储key
        V value;  // 存储value
        Entry<K,V> next;  // next指针,又来指向下一个节点
        final int hash;  // hash值
    }

从代码上可以看到,实现方法是使用静态内部类Entry来充当节点类的。next指针是当出现哈希冲突的时候,用来指向下一个节点的指针。

我们再看看其它的成员变量:

    // 默认初始化容量
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    // HashMap能够存储的最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默认负载因子,用来指示元素个数占整体容量的比例
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 用来存储键值对的Entry数组
    transient Entry[] table;

    // 存储实际元素个数
    transient int size;

    // 阈值=容量 * 负载因子。当超过这个数字,说明可能哈希冲突严重,需要扩容
    int threshold;

    // 实际的负载因子
    final float loadFactor;

    // 更新操作计数器,线程不安全时进行fail-fast机制
    transient int modCount;

成员变量需要注意的地方主要有两点:

  1. HashMap的容量在任何时刻都是2的n次幂,包括在扩容的时候也是扩容为原来的2倍。这主要是因为当容量为2的n次幂的时候,可以减少哈希碰撞的概率。具体可见HashMap初始容量为什么是2的n次幂及扩容为什么是2倍的形式。
  2. 阈值threshold=容量 * 负载因子,当超过阈值,可能有比较严重的哈希冲突,需要进行扩容。

3. 构造方法

    // 两个参数的构造方法
    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);

        // 传入的容量并不一定是2的n次幂,所以要找到最小的二次幂容量
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
        // 实际负载因子
        this.loadFactor = loadFactor;
        // 阈值
        threshold = (int)(capacity * loadFactor);
        // 根据容量创建数组
        table = new Entry[capacity];
        init();
    }

    // 一个参数的构造方法,使用默认负载因子0.75
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    // 无参构造方法,使用默认容量16,默认负载因子0.75
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }

    // Nap迁移
    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        putAllForCreate(m);
    }

可以看到,有4个构造方法,会使用2的n次幂来计算初始容量,并通过负载因子计算阈值。

4. put方法

    public V put(K key, V value) {
        // 如果key为null,单独处理
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());  // 计算hash值
        int i = indexFor(hash, table.length);  // 计算位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 相同的key进行覆盖
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;  // 更新次数+1
        addEntry(hash, key, value, i);  // 没有找到相同的
        return null;
    }

    // 如果key为null
    private V putForNullKey(V value) {
        // key为null的键值对,默认存放在0号位置
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            // 找到相同的key进行覆盖
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0); // 没有找到相同的
        return null;
    }

    // 扰动函数
    static int hash(int h) {
        // 通过位运算进行扰动,减少哈希冲突
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    // 计算Entry在数组中的位置
    static int indexFor(int h, int length) {
        // 和长度-1进行与运算,相当于进行模n计算。
        return h & (length-1);
    }

    // 添加Entry
    void addEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];  // 获取数组中bucketIndex的位置
        // 头插法插入元素
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        // 插入阈值就进行扩容,为2倍原来的大小
        if (size++ >= threshold)
            resize(2 * table.length);
    }

在上面的代码中,我们可以看到HashMap1.7中,是对于key为null的情况单独处理,放在
数组中0的位置上。我们对put方法的流程进行下总结

  1. 如果key为null,则单独处理。否则进入下一步;
  2. 通过扰动函数计算哈希值,并计算出要插入的位置i;
  3. 如果在i位置上找到了相同的key,则将value进行覆盖;如果没有找到相同的key,则创建新的Entry,利用头插法进行插入;
  4. 如果发现Entry个数超过阈值,则进入扩容,扩大容量为原来的2倍。

5. resize方法

    // 扩容的方法
    void resize(int newCapacity) {
        Entry[] oldTable = table;  // 旧的数组
        int oldCapacity = oldTable.length;  // 旧的容量
        if (oldCapacity == MAXIMUM_CAPACITY) { // 如果本身达到了最大的容量
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];  // 创建新的数组
        transfer(newTable);  // 迁移旧的Entry到扩容后的数组中
        table = newTable;   // 将table指向新的数组
        threshold = (int)(newCapacity * loadFactor);  // 重新计算阈值
    }

    // Entry进行迁移
    void transfer(Entry[] newTable) {
        Entry[] src = table;   // 旧数组
        int newCapacity = newTable.length;   // 新的容量
        for (int j = 0; j < src.length; j++) {  // 遍历旧数组中的每个桶
            Entry<K,V> e = src[j];  // 桶中的第一个元素
            if (e != null) {   // 如果不为null
                src[j] = null;
                do {
                    Entry<K,V> next = e.next;  // 先保存下一个要transfer的Entry
                    int i = indexFor(e.hash, newCapacity); // 计算新数组中的位置
                    e.next = newTable[i];  // 头插法插入元素
                    newTable[i] = e;  // 更新新数组中i位置第一个元素为e
                    e = next;  // e更新为旧数组中i位置下一个元素为e
                } while (e != null);
            }
        }
    }

在HashMap1.7扩容方法中,是将容量扩大为原来的2倍。扩容的过程如下:

  1. 创建2倍容量的新数组;
  2. 遍历旧数组中的每个桶,再遍历每个桶中的Entry,重新计算Entry在新数组中的位置,然后利用头插法的方式插入元素。

6. get方法

    // get元素
    public V get(Object key) {
        // key为null单独处理
        if (key == null)
            return getForNullKey();
        // 根据key计算hash值
        int hash = hash(key.hashCode());
        // 遍历桶中的Entry
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            // 如果发现相同的key,就返回value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

    // 如果key为null,就去数组中0位置去查找
    private V getForNullKey() {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

get方法就比较简单,先根据hash值计算出在数组中的位置,如果发现相同的key的Entry,就返回值。

7. remove方法

    // remove方法
    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }

    // 根据key删除Entry
    final Entry<K,V> removeEntryForKey(Object key) {
        // 计算hash值
        int hash = (key == null) ? 0 : hash(key.hashCode());
        // 计算数组中的位置
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];  // 前驱节点
        Entry<K,V> e = prev;  // 当前节点

        // 遍历桶中所有的Entry
        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            // 如果发现key相同的Entry
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;  // 元素个数-1
                if (prev == e)  // 如果删除的是头节点
                    table[i] = next;  // 那么下个节点就作为新的头节点
                else  // 删除的是中间节点
                    prev.next = next;  // 前驱节点指向下一个节点
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }

remove方法也比较简单,通过hash值计算出在数组中桶的位置,然后遍历桶中的Entry,找到相同的key就删除。

8. 扩容出现死循环

HashMap1.7是使用头插法插入节点的,在进行扩容调用resize方法,进而调用transfer方法迁移元素的时候,如果多线程并发,就有可能出现链表死循环的问题。具体描述可见Java:手把手带你源码分析 HashMap 1.7

参考文章:
Java:手把手带你源码分析 HashMap 1.7
为什么HashMap会产生死循环
HashMap1.7 最最最最最详细源码分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/129117.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vmware vcp证书怎么考?vmware vcp证书通过率如何

可为您提供行业领先的虚拟化技术培训和认证服务&#xff0c;这些认证不但会考察您的知识掌握情况和经验水平&#xff0c;还将与您的实际工作职责挂钩。VMware认证按照不同解决方案划分&#xff0c;可分为四条路径&#xff1a;数据中心虚拟化、网络虚拟化、云计算管理和自动化、…

微信小程序项目转uniapp踩坑日记

本文目录一、前言二、转换方式三、后语四、其他&#xff1a;node报错1、包默认C盘存放&#xff0c;而不是安装目录E盘2、正确的环境变量添加3、npm install 命令报错4、npm install -g express报错没有权限一、前言 由于想要把之前完成的微信小程序项目转换成uniapp项目&#…

git入门指南

文章目录Git入门指南前言什么是版本控制系统&#xff08;VCS&#xff09;版本控制系统Git1、概述2、目前比较流行的Git和SVN&#xff0c;区别是什么3、Git安装4、Git的工作区、暂存区、本地仓库、远程仓库5、git的分支6、git的标签7、 实际操作下git常用命令准备操作git cloneg…

Kafka Cluster 扩容 添加副本 重分配分区

Kafka Cluster 扩容 针对kafka集群&#xff0c;可以通过向群集添加新节点来扩展群集。新节点将仅服务于新主题或新分区&#xff0c;现有分区将不会自动重新平衡以使用新节点。如果需要对现有的TOPIC进行重新分配分区&#xff0c;需要运维人员手动进行干预。今天学习下如何对已…

CAPL学习之路-测试功能集函数(故障注入函数)

TestDisableMsg 禁止发送消息,除非调用函数TestSetMsgEvent 使用TestEnableMsg重新启用消息。此函数影响分配CANoe交互层或CANopen仿真的仿真节点 这个函数可以在测试用例中控制Simulation Setup界面仿真节点报文的发送与停止 testcase TCExample() {testDisableMsg(LightSt…

Linux模块代码、编译、加载、卸载一条龙

最近要写一个Linux的内核模块&#xff0c;记录一下内核模块的代码编写、编译、加载和卸载的基本流程&#xff0c;以作备忘&#xff0c;也希望能帮到有需要的同学。 模块代码 //代码来自https://yangkuncn.cn/kernel_INIT_WORK.html //init_works.c #include <linux/kernel…

Docker-compose快速部署PostgreSQL

Docker-compose快速部署PostgreSQL&#xff1a; 利用docker-compose编排工具部署&#xff1a; docker-compose.yml 文件 version: "3.1" services:postgresql:image: postgres:12-alpinecontainer_name: postgresqlenvironment:POSTGRES_DB: postgresPOSTGRES_USE…

Python--数据容器总结

一、数据容器的分类 数据容器可以从一下视角进行简单的分类&#xff1a; 是否支持下标索引 支持&#xff1a;列表、元组、字符串 --序列类型不支持&#xff1a;集合、字典 --非序列类型是否支持重复元素 支持&#xff1a;列表、元组、字符串 --序列类型不支持&#xff1a;集…

自动生成单测代码插件Squaretest

今天来介绍一款工具Squaretest&#xff0c;它是一款自动生成单元测试的插件&#xff0c;会用到它也是因为最近公司上了代码质量管控的指标&#xff0c;会考评各个项目的单元测试覆盖率&#xff0c;以及sonar扫描出来的各种问题。 很多老项目老代码&#xff0c;或者着急交付的项…

第十八讲:神州三层交换机DHCP中继服务的配置

当DHCP客户机和DHCP服务器不在同一个网段时&#xff0c;由DHCP中继传递DHCP报文。增加DHCP中继功能的好处是不必为每个网段都设置DHCP服务器&#xff0c;同一个DHCP服务器可以为很多个子网的客户机提供网络配置参数&#xff0c;即节约了成本又方便了管理。这就是DHCP中继的功能…

vue经历从2.0到3.0更新

​编辑vue专栏收录该内容 9 篇文章2 订阅 订阅专栏 vue经历从2.0到3.0更新之后&#xff0c;简⽽⾔之就是变得更轻&#xff0c;更快&#xff0c;使⽤起来更加⽅便&#xff0c;每⼀次的版本迭代都是对上⼀个版本的升级优化&#xff0c;不管 是对于我们开发者还是对于⽤户体验都…

铁威马NAS教程之使用CloudSync应用轻松同步备份网盘数据

铁威马在TOS 5系统中&#xff0c;将各种云盘的同步集合到了一个应用——CloudSync&#xff0c;更方便用户使用。只需要下载安装CloudSync&#xff0c;就可以在TNAS和各种云盘之间&#xff0c;实现快速安全的数据同步、分享文件。 1.TOS应用中心下载CloudSync应用&#xff1b; …

中国霍尔效应电流传感器市场规模达到了192.71百万美元?

霍尔效应&#xff0c;是指有小电流通过的一个半导体薄片置于磁场中&#xff0c;受到磁场作用影响电流发生偏转&#xff0c;在控制电流的垂直方向上的半导体两侧形成了电压差&#xff0c;该电势差就是霍尔电压。霍尔电压的大小&#xff0c;与磁场强度和半导体内通过的控制电流成…

JSP ssh 在线英语学习平台myeclipse开发oracle数据库MVC模式java编程计算机网页设计

一、源码特点 JSP ssh在线英语学习平台是一套完善的web设计系统&#xff08;系统采用ssh框架进行设计开发&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S 模式开发。开发环境为TOMCAT7.0…

【MySQL从入门到精通】【高级篇】(三十)记一次mysql5.7的新特性derived_merge的坑

您好&#xff0c;我是码农飞哥&#xff08;wei158556&#xff09;&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f4aa;&#x1f3fb; 1. Python基础专栏&#xff0c;基础知识一网打尽&#xff0c;9.9元买不了吃亏&#xff0c;买不了上当。 Python从入门到精…

深度学习 Day23——利用RNN实现天气预测

深度学习 Day23——利用RNN实现天气预测 文章目录深度学习 Day23——利用RNN实现天气预测一、前言二、我的环境三、前期工作1、导入依赖项2、导入数据3、查看数据基本信息4、查看各列数据的数据类型5、转换数据集中有关时间数据转换为时间格式6、删除Date标签列7、查看所有列的…

多版本并发控制(MVCC)

MVCC机制概述 MVCC&#xff08;Multi-Version Concurrency Control&#xff09;&#xff0c;中文是多版本并发控制&#xff0c;是指在使用READ COMMITTED、REPEATABLE READ这两种隔离级别的事务在执行SELECT操作时访问记录的版本链的过程&#xff0c;从而在不加锁的前提下使不…

基于LSTM三分类的文本情感分析,采用LSTM模型,训练一个能够识别文本postive, neutral, negative三种

基于LSTM三分类的文本情感分析&#xff0c;采用LSTM模型&#xff0c;训练一个能够识别文本postive, neutral, negative三种 &#xff0c;含数据集可直接运行 完整代码下载地址&#xff1a;基于LSTM三分类的文本情感分析 基于LSTM三分类的文本情感分析 背景介绍 文本情感分析…

Comic Life - 超棒的漫画制作工具,拥有多种动画模版,创作属于自己的漫画

Comic Life - 超棒的漫画制作工具&#xff0c;拥有多种动画模版&#xff0c;创作属于自己的漫画 Comic Life是一个照片编辑器&#xff0c;能够添加各种效果&#xff0c;并基于它们创建漫画。该工具包包括各种各样的模板&#xff0c;可以很容易地将照片放置在工作表上&#xff0…

CKEditor 为你的Flask项目添加一个富文本编辑器

人家高高在上的CKEditor是有个官网的&#xff01;&#xff01; WYSIWYG HTML Editor with Collaborative Rich Text EditingRock-solid, Free WYSIWYG Editor with Collaborative Editing, 200 features, Full Documentation and Support. Trusted by 20k companies.https://c…