Java HashMap源码剖析

字面上看,HashMap由Hash和Map两个单词组成,Map表示映射关系,是一个接口,实现Map接口有多种方式,HashMap实现的方式利用了Hash。本文先分析Map接口,接着分析HashMap实现原理,最后总结分析HashMap的特点。

目录

Map接口

HashMap实现原理

内部结构

构造方法

保存键值对

图解HashMap

get方法

删除键值对

HashMap性能


Map接口

Map有键和值的概念。一个键映射到一个值,Map按照键存储和访问值,键不能重复,即一个键只会存储一份,给同一个键重复设值会覆盖原来的值。使用Map可以方便地处理需要根据键访问对象的场景。

数组、ArrayList、LinkedList可以视为一种特殊的Map,键为索引,值为对象。

Map接口代码如下。Java 8增加了一些默认方法,如getOrDefault、forEach、replaceAIl、putlfAbsent、replace、computelfAbsent、merge等,Java9增加了多个重载的of方法,可以方便地根据一个或多个键值对构建不变的Map。

public interface Map<K, V> {
    V put(K key, V value);
    V get(Object key);
    V remove(Object key);
    int size();
    boolean isEmpty();
    boolean containsKey(Object key);
    boolean containsValue(Object value);
    void putAll(Map<? extends K, ? extends V> m);
    void clear();
    Set<K> keySet();
    Collection<V> values();
    Set<Map.Entry<K, V>> entrySet();

    interface Entry<K, V> {
        K getKey();
        V getValue();
        V setValue(V value);
        boolean equals(Object o);
        int hashCode();
    }

    boolean equals(Object o);
    int hashCode();
}

Set是一个接口,表示的是数学中的集合概念,即没有重复的元素集合。Java 中的Set扩展了Collection,但没有定义任何新的方法,不过,它要求所有实现者都必须确保Set的语义约束,即不能有重复元素。

Map中的键是没有重复的,所以ketSet()返回了一个Set。keySet()、values()、entrySet()有一个共同的特点,它们返回的都是视图,不是复制的值,基于返回值的修改会直接修改Map自身。

HashMap实现原理

内部结构

HashMap内部有如下几个主要的实例变量:

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
transient int size;
int threshold;
final float loadFactor;

size表示实际键值对的个数。table是一个Entry类型的数组,称为哈希表或哈希桶,其中的每个元素指向一个单向链表,链表中的每个节点表示一个键值对。Entry是一个内部类,它的实例变量和构造方法代码如下:

static class Entry<K,V> implements Map.Entry<K,V> {
       final K key;
       V value;
       Entry<K,V> next;
       int hash;
       Entry(int h, K k, V v, Entry<K,V> n) {
           value = v;
           next = n;
           key = k;
           hash = h;
       } 
}

其中,key和value分别表示键和值,next指向下一个Entry 节点,hash是key的hash值,待会我们会介绍其计算方法。直接存储hash值是为了在比较的时候加快计算。

table的初始值EMPTY_TABLE,是一个空表,具体定义:

static final Entry<?,?>[] EMPTY_TABLE = {};

当添加键值对后,table就不是空表了,它会随着键值对的添加进行扩展,扩展的策略类似于

ArrayList。添加第一个元素时,默认分配的大小为16,不过,并不是size大于16时再进行扩展,下次什么时候扩展与threshold有关。

threshold表示阈值,当键值对个数size大于等于threshold时考虑进行扩展。threshold是怎么算出来的呢?一般而言,threshold等于table length乘以loadFactor。比如,如果table.length为16,loadFactor为0.75,则threshold为12。loadFactor是负载因子,表示整体上table被占用的程度,是一个浮点数,默认为0.75,可以通过构造方法进行修改。

构造方法

HashMap默认构造方法的代码为:

public HashMap() {
       this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

DEFAULT_IINITIAL_CAPACITY 为16,DEFAULT_LOAD_FACTOR 0.75,默认构造方法调用的构造方法主要代码为:

public HashMap(int initialCapacity, float loadFactor) {
       this.loadFactor = loadFactor;
       threshold = initialCapacity;
}

保存键值对

下面,我们来看HashMap是如何把一个键值对保存起来的,代码为:

public V put(K key, V value) {
       if(table == EMPTY_TABLE) {
           inflateTable(threshold);
       }
       if(key == null)
           return putForNullKey(value);
       int hash = hash(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;
}

如果是第一次保存,首先调用inflateTable()方法给table分配实际的空间,inflateTable的主要代码为:

private void inflateTable(int toSize) {
       //Find a power of 2 >= toSize
       int capacity = roundUpToPowerOf2(toSize);
       threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
       table = new Entry[capacity];
}

默认情况下,capacity的值为16,threshold会变为12,table会分配一个长度为16的Entry数组。接下来,检查key是否null,如果是,调用putForNullKey单独处理,我们暂时忽略这种情况。在key不为null的情况下,下一步调用hash方法计算key的hash值。hash方法的代码为:

final int hash(Object k) {
       int h = 0
       h ^= k.hashCode();
       h ^= (h >>> 20) ^ (h >>> 12);
       return h ^ (h >>> 7) ^ (h >>> 4);
}

基于key自身的hashCode方法的返回值又进行了一些位运算,目的是为了随机和均匀性。有了hash值之后,调用indexFor方法,计算应该将这个键值对放到table的哪个位置,代码为:

static int indexFor(int h, int length) {
       return h & (length-1);
}

HashMap中,length 为2的幂次方,h& (length-1)等同于求模运算h%length。找到了保存位置i,table[i]指向一个单向链表。接下来,就是在这个链表中逐个查找是否已经有这个键了,遍历代码为:

 for (Entry<K,V> e = table[i]; e != null; e = e.next)

而比较的时候,是先比较hash值,hash相同的时候,再使用equals方法进行比较,代码为:

if(e.hash == hash && ((k = e.key) == key || key.equals(k)))

为什么要先比较hash呢?因为hash是整数,比较的性能一般要比equals高很多,hash不同,就没有必要调用equals方法了,这样整体上可以提高比较性能。如果能找到,直接修改Entry中的value即可。modCount++的含义与ArrayList和LinkedList中介绍一样,为记录修改次数,方便在迭代中检测结构性变化。如果没找到,则调用addEntry方法在给定的位置添加一条,代码为:

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 如果哈希表的大小超过阈值并且指定的桶索引位置已经存在元素,则进行扩容
    if (size >= threshold && table[bucketIndex] != null) {
        resize(2 * table.length);  // 对哈希表进行扩容
        hash = (key != null) ? hash(key) : 0;  // 计算哈希值
        bucketIndex = indexFor(hash, table.length);  // 计算新的桶索引
    }
    
    // 创建条目,传入哈希值、键、值和桶索引作为参数
    createEntry(hash, key, value, bucketIndex);
}

如果空间是够的,不需要resize,则调用createEntry方法添加。createEntry的代码为:

void createEntry(int hash, K key, V value, int bucketIndex) {
       Entry<K,V> e = table[bucketIndex];
       table[bucketIndex] = new Entry<>(hash, key, value, e);
       size++;
}

代码比较直接,新建一个Entry对象,插入单向链表的头部,并增加size。如果空间不够,即size已经要超过阈值threshold了,并且对应的table位置已经插入过对象了,具体检查代码为:

if((size >= threshold) && (null != table[bucketIndex]))

则调用resize方法对table进行扩展,扩展策略是乘2,resize的主要代码为:

void resize(int newCapacity) {
       Entry[] oldTable = table;
       int oldCapacity = oldTable.length;
       Entry[] newTable = new Entry[newCapacity];
       transfer(newTable, initHashSeedAsNeeded(newCapacity));
       table = newTable;
       threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

分配一个容量为原来两倍的Entry数组,调用transfer方法将原来的键值对移植过来,然后更新内部的table变量,以及threshold的值。参数rehash一般为false。这段代码遍历原来的每个键值对,计算新位置,并保存到新位置。transfer方法的代码为:

void transfer(Entry<K, V>[] 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);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

以上就是保存键值对的主要代码,简单总结一下,基本步骤为:

1)计算键的哈希值

2) 根据哈希值得到保存位置(取模)

3) 插到对应位置的链表头部或更新已有值

4) 根据需要扩展table大小

图解HashMap

在通过new HashMap()创建一个对象后,内存中的结构如图

接下来执行保存键值对的代码,“hello"的hash值为96207088,模16的结果为0,所以插入table[0]指向的链表头部,内存结构变为下图

"world”的hash值为111207038,模16结果为14,所以保存完"world"后,内存结构如图

"position”的hash值为771782464,模16结果也为0,table[0]已经有节点了,新节点会插到链表头部,内存结构变为如图所示

get方法

HashMap根据键获取值的get方法的代码为:

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

HashMap支持key为null,key为null的时候,放在table[O],调用getForNullKey()获取值;如果key不为null,则调用getEntry()获取键值对节点entry,然后调用节点的getValue()方法获取值。getEntry方法的代码是:

final Entry<K, V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }
    int hash = (key == null) ? 0 : hash(key);
    for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
        K k;
        if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
            return e;
        }
    }
    return null;
}

代码首先计算键的哈希值,然后根据哈希值找到哈希表中对应的链表。接着,在链表中遍历查找与给定键匹配的条目,通过快速比较哈希值和逐个比较键值来确定是否找到匹配的条目。

删除键值对

根据键删除键值对的代码为:

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

removeEntryForKey代码为:


   final Entry<K,V> removeEntryForKey(Object key) {
       if(size == 0) {
           return null;
       }
       int hash = (key == null) ? 0 : hash(key);
       int i = indexFor(hash, table.length);
       Entry<K,V> prev = table[i];
       Entry<K,V> e = prev;
       while(e != null) {
           Entry<K,V> next = e.next;
           Object k;
           if(e.hash == hash &&
               ((k = e.key) == key || (key != null && key.equals(k)))) {
               modCount++;
               size--;
               if(prev == e)
                   table[i] = next;
               else
                   prev.next = next;
               e.recordRemoval(this);
               return e;
           }
           prev = e;
           e = next;
       }
       return e;
 }

首先计算hash,根据hash找到对应的table索引,接着遍历table[i],查找待删节点,使用变量prev指向前一个节点,next指向后一个节点,e指向当前节点。然后判断是否找到,依然是先比较hash值,hash值相同时再用equals方法比较。删除的逻辑就是让长度减小,然后让待删节点的前后节点链起来,如果待删节点是第一个节点,则让table i直接指向后一个节点。

HashMap性能

1)根据键保存和获取值的效率都很高,为O(1),每个单向链表往往只有一个或少数几个节点,根据hash值就可以直接快速定位;

2) HashMap中的键值对没有顺序,因为hash值是随机的。

如果经常需要根据键存取值,而且不要求顺序,那么HashMap就是理想的选择。如果要保持添加的顺序,可以使用HashMap的一个子类LinkedHashMap。Map还有一个重要的实现类TreeMap,它可以排序。

需要说明的是,HashMap不是线程安全的,Java中还有一个类Hashtable,它是Java最早实现的容器类之一,实现了Map接口,实现原理与Hashap类似,但没有特别的优化,它内部通过synchronized实现了线程安全。在HashMap中,键和值都可以为null,而在Hashtable中不可以。在不需要并发安全的场景中,推荐使用HashMap。在高并发的场景中,推荐使用ConcurrentHashMap。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【云原生系列之kubernetes】--Ingress使用

service的缺点&#xff1a; 不支持基于URL等机制对HTTP/HTTPS协议进行高级路由、超时、重试、基于流量的灰度等高级流量治理机制难以将多个service流量统一管理 1.1ingress的概念 ingress是k8s中的一个对象&#xff0c;作用是如何将请求转发到service的规则ingress controlle…

STM32-启用蜂鸣器

目录 1 、电路构成及原理图 2、编写实现代码 main.c beep.c beep.h 3、代码讲解 4、 烧录到开发板调试、验证代码 5、检验效果 本人使用的是朗峰 STM32F103 系列开发板&#xff0c;此笔记基于这款开发板记录。 1 、电路构成及原理图 首先&#xff0c;通过朗峰 F1 开…

14. rk3588自带的RKNNLite检测yolo模型(python)

首先将文件夹~/rknpu2/runtime/RK3588/Linux/librknn_api/aarch64/下的文件librknnrt.so复制到文件夹/usr/lib/下&#xff08;该文件夹下原有的文件librknnrt.so是用来测试resnet50模型的&#xff0c;所以要替换成yolo模型的librknnrt.so&#xff09;&#xff0c;如下图所示&am…

相机图像质量研究(36)常见问题总结:编解码对成像的影响--块效应

系列文章目录 相机图像质量研究(1)Camera成像流程介绍 相机图像质量研究(2)ISP专用平台调优介绍 相机图像质量研究(3)图像质量测试介绍 相机图像质量研究(4)常见问题总结&#xff1a;光学结构对成像的影响--焦距 相机图像质量研究(5)常见问题总结&#xff1a;光学结构对成…

uniapp开发小程序项目

下载hbuilder 官网入口 下载地址 解压安装包 HBuilderX&#xff0c;Windows为zip包&#xff0c;解压后才能使用。 首先&#xff0c;选中下载的zip包&#xff0c;点击右键菜单&#xff0c;点击解压到当前文件夹进入解压后的文件夹&#xff0c;找到HBuilderX.exe&#xff0c;…

Leetcoder Day16| 二叉树 part05

语言&#xff1a;Java/C 513.找树左下角的值 给定一个二叉树的 根节点 root&#xff0c;请找出该二叉树的 最底层 最左边 节点的值。 假设二叉树中至少有一个节点。 示例 1: 输入: root [2,1,3] 输出: 1示例 2: 输入: [1,2,3,4,null,5,6,null,null,7] 输出: 7 本题需要满足两…

Flink/flinksql 语法 窗口与join 一文全 相关概念api汇总总结,底层process算子总结,与数据延迟处理,超时场景解决方案

Flink 窗口概念与join汇总总结 1 SQL语法中窗口语法相关&#xff08;仅仅是flinksql中 窗口的语法&#xff09;1.1 sql窗口1.2 window topN 2 java/SQL join语法与介绍2.1 有界join2.1.1 Window Join2.1.2 Interval Join2.1.3 Temporary Join2.1.4 LoopUp Join2.2 无界join2.2.…

MyBatis学习总结

MyBatis分页如何实现 分页分为 逻辑分页&#xff1a;查询出所有的数据缓存到内存里面&#xff0c;在从内存中筛选出需要的数据进行分页 物理分页&#xff1a;直接用数据库语法进行分页limit mybatis提供四种方法分页&#xff1a; 直接在sql语句中分页&#xff0c;传递分页参数…

js设计模式:原型模式

作用: 使用js特有的原型链机制,可以通过Object.create方法创建新对象,将一个对象作为另外一个对象的原型 也可以通过修改原型链上的属性,影响新对象的行为 可以更方便的创建一些对象 示例: let obj {getName: function(){return this.name},getAge:function(){return this…

【Flutter】底部导航BottomNavigationBar的使用

常用基本属性 属性名含义是否必须items底部导航栏的子项List是currentIndex当前显示索引否onTap底部导航栏的点击事件&#xff0c; Function(int)否type底部导航栏类型&#xff0c;定义 [BottomNavigationBar] 的布局和行为否selectedItemColor选中项图标和label的颜色否unsel…

[office] excel图表怎么发挥IF函数的威力 #微信#媒体

excel图表怎么发挥IF函数的威力 IF函数应该是最常用的Excel函数之一了&#xff0c;在公式中经常能够看到她的“身影”。IF函数的基本使用如图1所示。 图1 IF函数之美 IF函数是一个逻辑函数&#xff0c;通过判断提供相应操作&#xff0c;让Excel更具智能。 然而&#xff0c;…

Positive Technologies 确保 Rostic‘s 网络应用程序的安全

☑️ PT BlackBox分析 Rostics 网络应用程序的安全性 快餐连锁店在其安全网络开发过程中使用了我们的扫描仪。PT BlackBox 总共扫描了 20 多个 Rostics 的外部服务&#xff08;每天访问量超过 100,000 人次&#xff09;和企业服务&#xff08;每天访问量≈7,000 名员工&#x…

UE开发01--part 1:创建游戏模式、角色、控制器

1&#xff0c;右键选择新建C类 2&#xff0c;选择GameModeBase 3&#xff0c;随便命名&#xff0c;类的类型-->选择&#xff1a;公共&#xff1b; 这个选项会把.h和.cpp文件分开&#xff0c;方便我们查看与修改代码。 4.打开 VS 编辑器&#xff0c;查看我们刚刚创建得两文件…

windows安装以及切换使用nodejs多版本

1 安装nvm nvm是一个简单的bash脚本&#xff0c;它是用来管理系统中多个已存的Node.js版本。 可以先把系统已有的node卸载掉&#xff0c;也可不卸载&#xff0c;但是以防没必要的冲突&#xff0c;尽量还是卸掉。 1.1 下载nvm 下载地址&#xff1a;https://github.com/corey…

基于Python3的数据结构与算法 - 03 插入排序

类似于抽扑克牌&#xff1a; 初始时手里&#xff08;有序区&#xff09;只有一张牌每次&#xff08;从无序区&#xff09;摸一张牌&#xff0c;插入到手里已有牌的正确位置。 示例代码如下&#xff1a; def insert_sort(li):for i in range(1, len(li)): # i 表示摸到牌的下…

SAP PP学习笔记02 - PP中配置品目Master时的顺序

配置品目Master的时候&#xff0c;最佳实践是要遵循什么顺序呢&#xff1f; 一般而言是如下顺序 - 新规物料类型&#xff08;或利用现有类型也可以&#xff09; - 设定料号范围 - 设定物料状态&#xff08;比如准备好之前&#xff0c;要先锁住&#xff0c;等准备好了之后再…

HCIA-HarmonyOS设备开发认证V2.0-IOT硬件子系统-WatchDog

目录 一、 WATCHDOG 概述功能简介基本概念 二、WATCHDOG 模块相关API三、WATCHDOG HDF驱动开发3.1、开发步骤(待续...) 坚持就有收获 一、 WATCHDOG 概述 功能简介 看门狗&#xff08;Watchdog&#xff09;&#xff0c;又称看门狗计时器&#xff08;Watchdog timer&#xff0…

【AI大模型】ChatGPT在地学、GIS、气象、农业、生态、环境等领域中的高级应用

以ChatGPT、LLaMA、Gemini、DALLE、Midjourney、Stable Diffusion、星火大模型、文心一言、千问为代表AI大语言模型带来了新一波人工智能浪潮&#xff0c;可以面向科研选题、思维导图、数据清洗、统计分析、高级编程、代码调试、算法学习、论文检索、写作、翻译、润色、文献辅助…

miniblink简单demo分享

效果图&#xff1a; 通过wke.h和miniblink_4975_x32.dll进行环境的搭建。

【机器学习】数据清洗——基于Numpy库的方法删除重复点

&#x1f388;个人主页&#xff1a;豌豆射手^ &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;机器学习 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、交流进…