深入解析HashMap:结构与哈希函数揭秘一

news2024/11/18 6:42:03

文章目录

  • 一、HashMap的基本结构
    • 1.数组与链表的结构
      • 1.1 数组
      • 1.2 链表
    • 2.红黑树的简单介绍
    • 3.Node节点的组成
  • 二、HashMap的哈希函数
    • 1.hashCode()方法的作用
    • 2.位运算与哈希值的计算
    • 3.扰动函数的作用
  • 思考:为什么HashMap源码中使用位运算

在Java编程语言中,HashMap是一个至关重要的数据结构,它提供了键值对存储的高效方式。对于开发者来说,了解HashMap的内部工作原理不仅能够帮助提升编程技能,还能在实际应用中更好地利用这种数据结构。本文是“HashMap源码解析”的第一部分,我们将深入探讨HashMap的结构、哈希函数以及碰撞处理机制。

一、HashMap的基本结构

1.数组与链表的结构

1.1 数组

是一个线性表数据结构,它用一组连续的内存空间来存储一组具有相同类型的数据结构。

特点:

  • 静态数据结构:数组在创建时就有固定的大小,一旦创建,其大小不可改变。
  • 每一个元素都有索引,且是连续的,通过索引下标查询数据时间复杂度为O(1)。故,查询快~
  • 数组的大小是固定的,所以当数组需要插入、删除时,就涉及到了扩容,Java中数组是不支持动态扩容的。故,插入、删除慢~
  • 边界检查:Java数组在访问元素时会进行边界检查,如果越界,会抛出ArrayIndexOutOfBoundsException。

1.2 链表

一系列节点组成的集合,不要求连续的内存空间,每个节点存储着指向下个节点的引用

特点:

  • 不是连续的内存空间存储,查询时需要遍历全链表。故,查询慢O(n)~但是如果查询头节点、尾节点,可以直接通过方法获取O(1)
  • 链表的长度不是固定的,可以动态增加或减少
  • 链表插入或删除数据的时候,可以通过改变节点的指针实现。故,插入、删除快~时间复杂度O(1)
  • 无边界检查:链表不进行边界检查,访问节点时不会抛出ArrayIndexOutOfBoundsException。

2.红黑树的简单介绍

是一个黑色完美平衡的二叉树,即任意节点到每个叶子节点路径中黑色节点的数量相同,这一特性又称黑高

特点

  • 节点要么是红色,要么是黑色
  • 两个红色节点不能相连
  • 根节点一定是黑色的
  • 黑高
  • 每个叶子节点(NIL)是黑色的
    在这里插入图片描述

3.Node节点的组成

  • final int hash; # 节点键的hash值,HashMap使用这个哈希值来快速定位数组的索引,存储和检索键值对
  • final K key; # 节点key,键是唯一标识,用于获取值
  • V value; # 节点键对应的值
  • Node<K,V> next; # 节点指向的下一个节点
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> 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;

            return o instanceof Map.Entry<?, ?> e
                    && Objects.equals(key, e.getKey())
                    && Objects.equals(value, e.getValue());
        }
    }

二、HashMap的哈希函数

1.hashCode()方法的作用

  • hashCode()是Object类的方法
  • 通过hashCode()方法可以进行字符串加密生成哈希码,且不可逆。通过哈希码确定键值对的位置

理想情况下,hashCode()方法应该为不同的对象生成不同的哈希码,以减少哈希冲突的概率。然而,在实际应用中,可能会出现不同的对象产生相同哈希码的情况,这种现象称为哈希冲突。为了处理哈希冲突,HashMap使用了链表和红黑树的数据结构来存储具有相同或相近哈希码的键值对。

2.位运算与哈希值的计算

在HashMap的源码中大量用到了位运算与运算的地方。
在HashMap中,hashCode()方法返回的哈希码通常是一个整型值。为了将这个哈希码转换成内部数组的索引,HashMap使用了位运算。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这里的^是按位异或运算符,>>>是无符号右移运算符。
这种运算可以确保哈希码的高位和低位都被用于索引的计算,从而减少哈希冲突的概率。

3.扰动函数的作用

HashMap的hash方法用于计算键的哈希码,并将其转换为哈希表的索引。这个方法确实使用了位运算来扰动哈希码,但是具体的实现稍微有所不同。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这个方法会首先计算键的哈希码h(h = key.hashCode()),然后将其无符号右移16位(h >>> 16),并与原始哈希码进行异或运算。这样做的目的是将高位的特征和低位的特征混合起来,以减少哈希冲突
然而,这个扰动的哈希码并不直接用作数组的索引。在Java 8中,HashMap的数组长度必须是2的幂,因此数组的索引是通过下面的方式计算的:

(n - 1) & hash

这里的n是数组的长度,hash是上面计算的扰动哈希码。(n - 1)是一个所有位都是1的二进制数(例如,如果n是16,那么(n - 1)0b1111),这样做的效果是取模运算,但是位运算通常比模运算快得多。
在Java 8中,还有一个重要的优化,当链表的长度超过一定阈值时,链表会转换为红黑树,以提高性能。

思考:为什么HashMap源码中使用位运算

在HashMap中使用位运算是为了提高哈希分布的均匀性,减少哈希冲突的概率,从而提高整体的性能。以下是使用位运算的几个原因:

  1. 速度:位运算通常比其他数学运算要快,因为它们直接在数字的二进制表示上操作,而现代计算机的CPU对位运算有直接的硬件支持。
  2. 均匀分布:通过位运算,可以将哈希码的高位和低位混合,从而使得哈希码的每个位都对最终的索引结果产生影响。这样可以减少哈希码中某些位总是为零或总是为一的情况,使得哈希值更加均匀地分布在哈希表中。
  3. 减少哈希冲突:哈希冲突是指两个不同的键产生了相同的哈希值。通过位运算,可以增加哈希值的独特性,减少冲突的可能性。
  4. 适应不同容量:HashMap的容量通常是2的幂次方,使用位运算可以很容易地计算出哈希值在哈希表中的位置。例如,如果哈希表的容量是2^n,那么可以通过hash & (2^n - 1)来计算索引,这里的&是按位与运算符,它等价于hash % 2^n,但执行速度更快。
  5. 利用CPU缓存:现代CPU缓存行通常是一次性加载一块数据,而位运算可以帮助优化数据的内存布局,使得哈希表中的元素更可能被加载到同一缓存行中,从而提高缓存利用率。

在HashMap中,扰动函数通常包括将哈希码的高16位与低16位进行异或运算,这样可以确保哈希码的每个位都对索引结果有影响,从而提高哈希表的性能。

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

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

相关文章

『 Linux 』进程替换( Process replacement ) 及 简单Shell的实现(万字)

文章目录 &#x1f984; 进程替换&#x1f9a9; execl()函数&#x1f9a9; execlp()函数&#x1f9a9; execle()函数&#x1f9a9; execv()函数&#x1f9a9; execvp()函数&#x1f9a9; execvpe()函数&#x1f9a9; execve()函数 &#x1f984; 简单Shell命令行解释器的实现&a…

区块链基础知识(上):区块链基本原理、加密哈希、公钥加密

目录 基本原理 加密哈希&#xff1a; 公钥加密&#xff1a; 希望有人向你发送只有你才能打开的加密文档/消息时使用 PKC 希望向其他人发送加密文档/消息并证明它确实由你发送时使用 PKC 使用 PKC 和加密哈希对文档/消息进行数字签名 交易哈希链使用数字签名转让数字资产所…

Docker进阶:深入了解 Dockerfile

Docker进阶&#xff1a;深入了解 Dockerfile 一、Dockerfile 概述二、Dockerfile 优点三、Dockerfile 编写规则四、Dockerfile 中常用的指令1、FROM2、LABEL3、RUN4、CMD5、ENTRYPOINT6、COPY7、ADD8、WORKDIR9、 ENV10、EXPOSE11、VOLUME12、USER13、注释14、ONBUILD 命令15、…

算法刷题Day9 | 28. 实现 strStr()、459.重复的子字符串、字符串总结

目录 0 引言1 实现 strStr()1.1 我的解题1.2 KMP算法解题 2 重复的子字符串2.1 暴力求解2.2 KMP求解法 3 字符串总结 &#x1f64b;‍♂️ 作者&#xff1a;海码007&#x1f4dc; 专栏&#xff1a;算法专栏&#x1f4a5; 标题&#xff1a;算法刷题Day8 | 28. 实现 strStr()、45…

画图实战-Python实现某产品全年销量数据多种样式可视化

画图实战-Python实现某产品全年销量数据多种样式可视化 学习心得Matplotlib说明什么是Matplotlib&#xff1f;Matplotlib特性Matplotlib安装 产品订单量-折线图某产品全年订单量数据数据提取和分析绘制折线图 产品订单&销售额-条形图某产品全年订单&销售额数据绘制条形…

【嵌入式学习】C++day03.14

一、思维导图 二、练习 成员函数版本实现算术运算符的重载 全局函数版本实现算术运算符的重载 #include <iostream>using namespace std;class Num {friend const Num operator-(const Num &L,const Num &R); private:int a;int b; public://运算符重载const …

【Linux系统编程】进程的退出与等待

进程的创建 fork()用于创建子进程。但fork创建的子进程获得的是父进程&#xff08;即调用 fork() 的进程&#xff09;的一份几乎完全相同的副本&#xff0c;包括父进程的代码、数据、堆、栈和数据结构等内容。当进程调用fork后&#xff0c;一旦控制转移到内核中的fork代码后&am…

ATTRIBUTE_HELPER_HEADER

ATTRIBUTE_HELPER_HEADER是ns3中的一个宏定义&#xff0c;用于声明类类型的属性值、访问器和检查器。 例如&#xff1a; ATTRIBUTE_HELPER_HEADER (QueueSize);此宏声明&#xff1a; 属性值类typeValue&#xff0c;属性访问器函数MaketypeAccessor&#xff0c;AttributeChec…

CesiumJS 沙盒

CesiumJS 沙盒 通过CesiumJS 沙盒快速测试CesiumJS的一些功能&#xff0c;免去安装开发环境的困恼。 Hello World https://sandcastle.cesium.com/index.html 简单修改&#xff08;F8运行&#xff09;&#xff1a;去掉界面上UI const viewer new Cesium.Viewer("cesi…

C++中的STL-string类

文章目录 一、为什么学习string类&#xff1f;1.1 C语言中的字符串 二、准库中的string类2.2 string类2.3 string类的常用接口说明2.4 string类对象的容量操作2.5 string类对象的访问及遍历操作2.5 string类对象的修改操作2.7 string类非成员函数2.8 模拟实现string 一、为什么…

C++特性之一:继承

1. 派生类的成员变量、成员函数、构造、析构 2. 继承的切片 3. 重定义/隐藏 重定义/隐藏&#xff1a;派生类和基类有同名的成员&#xff0c;就叫隐藏。派生类的成员隐藏了基类的成员。 隐藏时可以通过类作用限定符来访问被隐藏的成员。 class Person { public:void Print(){…

一文了解Spring的SPI机制

文章目录 一文了解Spring的SPI机制Java SPIServiceLoader Spring SPISpringboot利用Spring SPI开发starter 一文了解Spring的SPI机制 Java SPI SPI 全称 Service Provider Interface &#xff0c;是 Java提供的一套用来被第三方实现或者扩展的接口&#xff0c;它可以用来启用…

考研数学——高数:高斯公式

助记: 关于积分时什么时候可以将变量整体代入积分式的问题&#xff1a;在积分过程中&#xff0c;如果某一整体恒为常量&#xff0c;则可以直接替换为定值&#xff0c;常见于对线或面的积分。 而在这题&#xff0c;用高斯公式之前是面积分&#xff0c;如果有这个整体出现的话是…

寒假作业Day 11

寒假作业Day 11 一、选择题 栈满的判断&#xff1a;在链式存储结构中&#xff0c;栈的大小是动态的&#xff0c;只受限于系统分配给程序的内存大小。因此&#xff0c;理论上&#xff0c;链式栈不会因为空间不足而“满”。所以&#xff0c;不需要判断栈满。 栈空的判断&#xf…

解决VS编译中文报错 error C2001:常量中有换行符

产生原因&#xff1a;文件中有中文字符&#xff0c;但是文件是utf-8格式的&#xff0c;使用msvc编译器编译时就会产生上述错误 首先说明&#xff1a;我是通过方法2解决该问题的。 解决办法&#xff1a; 方式1&#xff1a; 通过把源文件转换为gbk编码&#xff0c;但是只能一…

TCP和UDP基础

tcp服务器及客户端链接 ucd服务器及客户端

python自学7

第二章第一节面向对象 程序的格式都不一样&#xff0c;每个人填写的方式也有自己的习惯&#xff0c;比如收集个人信息&#xff0c;可能有人用字典字符串或者列表&#xff0c; 类的成员方法 类和对象 构造方法 挨个传输值太麻烦了&#xff0c;也没有方便点的&#xff0c;有&…

4_springboot_shiro_jwt_多端认证鉴权_Redis存储会话

1. 什么是会话 所谓的会话&#xff0c;就是用户与应用程序在某段时间内的一系列交互&#xff0c;在这段时间内应用能识别当前访问的用户是谁&#xff0c;而且多次交互中可以共享数据。我们把一段时间内的多次交互叫做一次会话。 即用户登录认证后&#xff0c;多次与应用进行交…

WPF —— Calendar日历控件详解

1&#xff1a; Calendar的简介 日历控件用于创建可视日历&#xff0c;让用户选择日期并在选择日期时触发事件。 DisplayMode 用来调整日历显示模式&#xff0c;分为Month、Year 和Decade 三种。如下是None 2&#xff1a;Calendar控件常用的属性 SelectionMode 选中日历的类…

原生php单元测试示例

下载phpunit.phar https://phpunit.de/getting-started/phpunit-9.html 官网 然后win点击这里下载 新建目录 这里目录可以作为参考&#xff0c;然后放在根目录下 新建一个示例类 <?phpdeclare(strict_types1);namespace Hjj\DesignPatterns\Creational\Hello;class He…