JDK7 HashMap

2023-12-04

在Java中HashMap是一个常用且重要的容器,它基于哈希表实现,提供了高效的插入、删除和查找操作.本文我们将分别讲述JDK7中的HashMap。

使用

HashMap的使用非常简单,下面演示下存数据与取数据.

// 简单示例
public static void main(String[] args) {
        // 创建HashMap
        HashMap<Object, Object> map = new HashMap<>();

        // 存数据,key-value
        map.put("1", "rlj");
        map.put("2", "mk");
        map.put("3", "ali");

        // 取数据,根据key获取对应value
        System.out.println(map.get("1"));
        System.out.println(map.get("2"));
        System.out.println(map.get("3"));

    }
// 输出
rlj
mk
ali

底层结构

JDK7中HashMap的底层是一个数组,每个数组元素是一个链表.
在这里插入图片描述

// enttry组成
 K key; // 键值队中的key
 V value; // 键值队中的value
 Entry<K,V> next; // 下一个Entry对象
 int hash; // key的hash值

构造方法

这里我们只看它的默认无参构造方法

// 默认数组大小16,加载因子0.75
public HashMap() {
  this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

put元素

下图为HashMap储存一个新元素的过程
在这里插入图片描述

public V put(K key, V value) {
		// 第一次插入Map是空的,需要初始化数组
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        // 如果key = null,单独处理
        if (key == null)
            return putForNullKey(value);
        // 计算key的哈希值
        int hash = hash(key);
        // 计算key对应的数组索引
        int i = indexFor(hash, table.length);
        // 遍历对应位置的链表(如果存在)
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 如果找到相同的键,则更新其对应的值,并返回旧值
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        // 如果没有找到相同的键,将新的键值对插入到链表的头部
        addEntry(hash, key, value, i);
        return null;
    }

下面我们来逐步解析:

初始化数组
private void inflateTable(int toSize) {
        // 根据传入的 toSize 参数,找到大于等于 toSize 的最小的 2 的幂次方值作为数组的容量。
        // 这个操作确保数组的大小总是 2 的幂次方
        int capacity = roundUpToPowerOf2(toSize);
        // 根据数组的容量和加载因子(loadFactor)计算阈值
        // 当 HashMap 中的元素个数超过了阈值时,就会触发扩容
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        // 根据计算得到的容量,创建一个新的 Entry 数组作为 HashMap 的存储空间
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }
// 总结
1.根据toSize(默认16)计算数组的大小capacity,且capacity必须是2的次方幂(大于等于传入的toSize)
2.根据capacity和加载因子计算扩容的阈值
3.初始化新数组,大小为capacity
正常key处理

先来看正常key的处理逻辑

// 1.计算key的哈希值
// ->  int hash = hash(key);
这个方法计算key的hash值,并进行三次向右位移和异或操作,将哈希值的高位和低位进行了混合,
以增加哈希码的随机性,使得元素在 HashMap 中的存储位置更加均匀、分布更广泛.
// 2.计算key对应的数组索引
// -> int i = indexFor(hash, table.length);
// ->
 static int indexFor(int h, int length) {
        return h & (length-1);
    }
// 用于根据哈希值和数组长度计算哈希值在数组中的索引位置。
// 因为数组的大小length都是2的幂次方,所以length-1的二进制所有位都是1,通过与运算可以直接取哈希值的低位作为数组索引。这样可以快速定位数组索引.
// 3.替换旧值
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
  if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
// 计算出索引下标后,拿到对应Entry对象
// 遍历entry链表,若存在相同的key值,则替换对应的value,并且返回旧值
// 4.增加新节点
// -> addEntry(hash, key, value, i);
 void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }
// 在这个方法主要做了这些事:
1.若满足 当前数组大小大于等于阈值 && 当前要插入位置entry不为null -> 触发扩容
2.若发生了扩容,则重新计算key的hash值,根据新数组的长度计算数组索引
3.将新节点插入 -> createEntry()
// -> 这里是头插法
void createEntry(int hash, K key, V value, int bucketIndex) {
		// 取出当前数组所在的第一个entry对象e
        Entry<K,V> e = table[bucketIndex];
        // 新创建的entry的next为e
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
// 举例:若当时链表为A-B,插入C后则为 -> C-A-B

下面我们来详细讲述下,HashMap的扩容机制

// 5.扩容
// -> resize(2 * table.length);
// ->
 void resize(int newCapacity) {
 		// 取出旧的hash表
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        // 若旧的hash表长度已经为最大值,则不会再进行扩容
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
		// 创建新数组,长度为旧数组的2倍
        Entry[] newTable = new Entry[newCapacity];
        // 将旧数组中的元素转移到新数组中
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        // 新数组作为最新的hash表
        table = newTable;
        // 重新计算阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

// -> 转移元素
// transfer(newTable, initHashSeedAsNeeded(newCapacity));
// ->
void transfer(Entry[] newTable, boolean rehash) {
		// 获取新数组长度
        int newCapacity = newTable.length;
        // 遍历旧数组全部元素转移
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                // 重新计算hash
                int i = indexFor(e.hash, newCapacity);
                // 头插法插入到新数组中
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
// 根据上述代码:
1.hashMap扩容到目的主要两个:增加数组的长度、减少链表的长度
2.从旧数组到新数组,因为重新计算了hash,所以元素对应的数组下标可能发生改变了.
3.转移使用头插法,所以链表的顺序会相反

可以根据下图方便理解-元素转移示例图:
在这里插入图片描述

null key处理

从下面代码,我们可以知道HashMap的key是可以为null的,并且key=null的entry是存储在数组的第一索引位置的.其他逻辑类似正常key.

if (key == null)
   return putForNullKey(value);
// -> 存储在第一个节点
 private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }
线程安全问题

HashMap是线程不安全的,就比如上述扩容过程中,如果有多个线程操作,可能导致底层链表循环,从而导致后续操作map导致死循环.

get元素

在这里插入图片描述

public V get(Object key) {
  if (key == null)
     return getForNullKey();
    Entry<K,V> entry = getEntry(key);
    return null == entry ? null : entry.getValue();
}

// ->getEntry(key)
 final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
		// 计算hash值
        int hash = (key == null) ? 0 : hash(key);
        // 定位到对应数组
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

总结

以上便是JDK7 HashMap的底层存储逻辑,后续我们再来研究 JDK8的HashMap >https://blog.csdn.net/2302_77659577/article/details/134690427 底层逻辑,比较两者之间的区别.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JDK7 HashMap 的相关文章

随机推荐

  • 界面组件DevExpress Reporting v23.1新版亮点 - UX功能增强

    DevExpress Reporting gt https www evget com product 3373 是 NET Framework下功能完善的报表平台 它附带了易于使用的Visual Studio报表设计器和丰富的报表控件集
  • 扬帆证券:趋势线是画最低点还是收盘价?

    趋势线是股票分析中最底子的技术指标之一 趋势线是一种可帮忙股票生意者辨认价格趋势的图形方法 趋势线是可以经过联接恣意两个价格点画出的一条直线 但是 在画出趋势线时 一个常见的问题是 运用最低点还是收盘价来画趋势线 在这篇文章中 我们将从多个
  • 【计算机毕业设计】垃圾分类回收系统

    垃圾分类回收系统 如今社会上各行各业 都喜欢用自己行业的专属软件工作 互联网发展到这个时候 人们已经发现离不开了互联网 新技术的产生 往往能解决一些老技术的弊端问题 因为传统垃圾分类回收系统信息管理难度大 容错率低 管理人员处理数据费工费时
  • 扬帆证券:A股股息率逼近历史新高 价值股迎配置良机

    A股公司酬谢股东积极性持续进步 本年前三季度多达243家公司现金分红 一切核算数据只包括各个报告期分红 不包括特别分红 派现公司数量及占比均创出10年来同期新高 估计分红近2300亿元 分红率靠近33 分红额及分红率均为近10年来同期次高
  • 【计算机毕业设计】民航网上订票系统

    民航网上订票系统 传统办法管理信息首先需要花费的时间比较多 其次数据出错率比较高 而且对错误的数据进行更改也比较困难 最后 检索数据费事费力 因此 在计算机上安装民航网上订票系统软件来发挥其高效地信息处理的作用 可以规范信息管理流程 让管理
  • SQL 数据操作技巧:SELECT INTO、INSERT INTO SELECT 和 CASE 语句详解

    SQL SELECT INTO 语句 SELECT INTO 语句将数据从一个表复制到一个新表中 SELECT INTO 语法 将所有列复制到新表中 SELECT INTO newtable IN externaldb FROM oldta
  • 双馈风机虚拟惯性控制+下垂控制参与系统一次调频的Simulink模型

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 参考文献 4 Simulink仿真实现
  • 邻接表表示图进行深度优先搜索,广度优先搜索,最小生成树

    图的邻接表定义 下面用邻接表实现图的深度优先搜索和广度优先搜索 用邻接矩阵来实现最小生成树 图的邻接表 首先定义一个图的邻接表的类 里面包括图的顶点数 图的边数 顶点表数组 由于顶点表数组里存放的都是图的一个个节点 因此需要用到顶点节点和边
  • 【计算机毕业设计】宠物猫认养系统

    宠物猫认养系统 传统办法管理信息首先需要花费的时间比较多 其次数据出错率比较高 而且对错误的数据进行更改也比较困难 最后 检索数据费事费力 因此 在计算机上安装宠物猫认养系统软件来发挥其高效地信息处理的作用 可以规范信息管理流程 让管理工作
  • 扬帆证券:什么是证券服务机构?

    股票市场上 除出资者之外有各式各样的生意主体 盘绕证券所打开的公司类型非常丰盛 在实践生意中 常与出资者有所联络的不止证券公司 还有证券服务组织 那什么是证券服务组织 它和证券公司之间有什么关系 关于这些 本文将借用相关常识作部分评论 来为
  • 基于GWO-BP灰狼算法优化BP神经网络时序回归预测研究(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 目录 1 概述 2 运行结果 3 参考文献 4 Matlab代码 数据 讲解文档 1 概述 基于GWO
  • 【计算机毕业设计】红色革命文物征集管理系统

    红色革命文物征集管理系统 传统办法管理信息首先需要花费的时间比较多 其次数据出错率比较高 而且对错误的数据进行更改也比较困难 最后 检索数据费事费力 因此 在计算机上安装红色革命文物征集管理系统软件来发挥其高效地信息处理的作用 可以规范信息
  • 【计算机毕业设计】私房菜定制上门服务系统

    私房菜定制上门服务系统 如今社会上各行各业 都喜欢用自己行业的专属软件工作 互联网发展到这个时候 人们已经发现离不开了互联网 新技术的产生 往往能解决一些老技术的弊端问题 因为传统私房菜定制上门服务系统信息管理难度大 容错率低 管理人员处理
  • 光伏储能单相逆变器并网仿真模型(Simulink仿真实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 参考文献 4 Simulink仿真实现
  • 【计算机毕业设计】视频点播系统

    视频点播系统 传统办法管理信息首先需要花费的时间比较多 其次数据出错率比较高 而且对错误的数据进行更改也比较困难 最后 检索数据费事费力 因此 在计算机上安装视频点播系统软件来发挥其高效地信息处理的作用 可以规范信息管理流程 让管理工作可以
  • 【计算机毕业设计】可信捐赠系统

    可信捐赠系统 如今社会上各行各业 都喜欢用自己行业的专属软件工作 互联网发展到这个时候 人们已经发现离不开了互联网 新技术的产生 往往能解决一些老技术的弊端问题 因为传统可信捐赠系统信息管理难度大 容错率低 管理人员处理数据费工费时 所以专
  • 一文讲透Python线程池ThreadPoolExecutor

    01 初识 Python 中已经有了 threading 模块 为什么还需要线程池呢 线程池又是什么东西呢 在介绍线程同步的信号量机制的时候 举得例子是爬虫的例子 需要控制同时爬取的线程数 例子中创建了20个线程 而同时只允许3个线程在运行
  • 【计算机毕业设计】健身房管理系统

    健身房管理系统 传统办法管理信息首先需要花费的时间比较多 其次数据出错率比较高 而且对错误的数据进行更改也比较困难 最后 检索数据费事费力 因此 在计算机上安装健身房管理系统软件来发挥其高效地信息处理的作用 可以规范信息管理流程 让管理工作
  • 扬帆证券:三大项目启动 深圳打造金融科技发展高地

    11月29日 2023深圳国际金融科技节正式拉开帷幕 作为金融科技节的中心板块 2023我国 深圳 金融科技大会也于当日举办 记者从现场了解到 本届大会愈加集合金融科技使用范畴 推出多个 实招 促进金融科技落地使用和探寻打开远景 详细来看
  • JDK7 HashMap

    在Java中HashMap是一个常用且重要的容器 它基于哈希表实现 提供了高效的插入 删除和查找操作 本文我们将分别讲述JDK7中的HashMap 使用 HashMap的使用非常简单 下面演示下存数据与取数据 简单示例 public sta