HashMap的实现原理(看这篇就够了)
一线资深java工程师明确了需要精通集合容器,尤其是今天我谈到的HashMap。
HashMap在Java集合的重要性不亚于Volatile在并发编程的重要性(可见性与有序性)。
我会重点讲解以下9点:
1.HashMap的数据结构
2.HashMap核心成员
3.HashMapd的Node数组
4.HashMap的数据存储
5.HashMap的哈希函数
6.哈希冲突:链式哈希表
7.HashMap的get方法:哈希函数
8.HashMap的put方法
9.为什么槽位数必须使用2^n?
HashMap的数据结构
首先我们从数据结构的角度来看:HashMap是:数组+链表+红黑树(JDK1.8增加了红黑树部分)的数据结构,如下所示:
这里需要搞明白两个问题:
- 数据底层具体存储的是什么?
- 这样的存储方式有什么优点呢?
1.核心成员
默认初始容量(数组默认大小):16,2的整数次方 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; 默认负载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; 装载因子用来衡量HashMap满的程度,表示当map集合中存储的数据达到当前数组大小的75%则需要进行扩容 链表转红黑树边界 static final int TREEIFY_THRESHOLD = 8; 红黑树转离链表边界 static final int UNTREEIFY_THRESHOLD = 6; 哈希桶数组 transient Node<K,V>[] table; 实际存储的元素个数 transient int size; 当map里面的数据大于这个threshold就会进行扩容 int threshold 阈值 = table.length * loadFactor
2.Node数组
从源码可知,HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组,明显它是一个Node的数组。
static class Node<K,V> implements Map.Entry<K,V> { final int hash;//用来定位数组索引位置 final K key; V value; Node<K,V> next;//链表的下一个Node节点 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; 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; } }
Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。
HashMap的数据存储
1.哈希表来存储
HashMap采用哈希表来存储数据。
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构,只要输入待查找的值即key,即可查找到其对应的值。
哈希表其实就是数组的一种扩展,由数组演化而来。可以说,如果没有数组,就没有散列表。
2.哈希函数
哈希表中元素是由哈希函数确定的,将数据元素的关键字Key作为自变量,通过一定的函数关系(称为哈希函数),计算出的值,即为该元素的存储地址。
表示为:Addr = H(key),如下图所示:
哈希表中哈希函数的设计是相当重要的,这也是建哈希表过程中的关键问题之一。
3.核心问题
建立一个哈希表之前需要解决两个主要问题:
1)构造一个合适的哈希函数,均匀性 H(key)的值均匀分布在哈希表中
2)冲突的处理
冲突:在哈希表中,不同的关键字值对应到同一个存储位置的现象。
4.哈希冲突:链式哈希表
哈希表为解决冲突,可以采用地址法和链地址法等来解决问题,Java中HashMap采用了链地址法。
链地址法,简单来说,就是数组加链表的结合,如下图所示:
HashMap的哈希函数
/** * 重新计算哈希值 */ static final int hash(Object key) { int h; // h = key.hashCode() 为第一步 取hashCode值 // h ^ (h >>> 16) 为第二步 高位参与运算 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
//计算数组槽位
(n - 1) & hash
对key进行了hashCode运算,得到一个32位的int值h,然后用h 异或 h>>>16位。在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16)。
这样做的好处是,可以将hashcode高位和低位的值进行混合做异或运算,而且混合后,低位的信息中加入了高位的信息,这样高位的信息被变相的保留了下来。
等于说计算下标时把hash的高16位也参与进来了,掺杂的元素多了,那么生成的hash值的随机性会增大,减少了hash碰撞。
备注:
- ^异或:不同为1,相同为0
- >>> :无符号右移:右边补0
- &运算:两位同时为“1”,结果才为“1,否则为0
h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方。
为什么槽位数必须使用2^n?
1.为了让哈希后的结果更加均匀
假如槽位数不是16,而是17,则槽位计算公式变成:(17 – 1) & hash
从上文可以看出,计算结果将会大大趋同,hashcode参加&运算后被更多位的0屏蔽,计算结果只剩下两种0和16,这对于hashmap来说是一种灾难。2.等价于length取模
当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
位运算的运算效率高于算术运算,原因是算术运算还是会被转化为位运算。
最终目的还是为了让哈希后的结果更均匀的分部,减少哈希碰撞,提升hashmap的运行效率。
分析HashMap的put方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 当前对象的数组是null 或者数组长度时0时,则需要初始化数组 if ((tab = table) == null || (n = tab.length) == 0) { n = (tab = resize()).length; } // 使用hash与数组长度减一的值进行异或得到分散的数组下标,预示着按照计算现在的 // key会存放到这个位置上,如果这个位置上没有值,那么直接新建k-v节点存放 // 其中长度n是一个2的幂次数 if ((p = tab[i = (n - 1) & hash]) == null) { tab[i] = newNode(hash, key, value, null); } // 如果走到else这一步,说明key索引到的数组位置上已经存在内容,即出现了碰撞 // 这个时候需要更为复杂处理碰撞的方式来处理,如链表和树 else { Node<K,V> e; K k; //节点key存在,直接覆盖value if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) { e = p; } // 判断该链为红黑树 else if (p instanceof TreeNode) { // 其中this表示当前HashMap, tab为map中的数组 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); } else { // 判断该链为链表 for (int binCount = 0; ; ++binCount) { // 如果当前碰撞到的节点没有后续节点,则直接新建节点并追加 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // TREEIFY_THRESHOLD = 8 // 从0开始的,如果到了7则说明满8了,这个时候就需要转 // 重新确定是否是扩容还是转用红黑树了 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // 找到了碰撞节点中,key完全相等的节点,则用新节点替换老节点 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 此时的e是保存的被碰撞的那个节点,即老节点 if (e != null) { // existing mapping for key V oldValue = e.value; // onlyIfAbsent是方法的调用参数,表示是否替换已存在的值, // 在默认的put方法中这个值是false,所以这里会用新值替换旧值 if (!onlyIfAbsent || oldValue == null) e.value = value; // Callbacks to allow LinkedHashMap post-actions afterNodeAccess(e); return oldValue; } } // map变更性操作计数器 // 比如map结构化的变更像内容增减或者rehash,这将直接导致外部map的并发 // 迭代引起fail-fast问题,该值就是比较的基础 ++modCount; // size即map中包括k-v数量的多少 // 超过最大容量 就扩容 if (++size > threshold) resize(); // Callbacks to allow LinkedHashMap post-actions afterNodeInsertion(evict); return null; }
HashMap的put方法执行过程整体如下:
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
HashMap总结
HashMap底层结构?基于Map接口的实现,数组+链表的结构,JDK 1.8后加入了红黑树,链表长度>8变红黑树,<6变链表
两个对象的hashcode相同会发生什么? Hash冲突,HashMap通过链表来解决hash冲突
HashMap 中 equals() 和 hashCode() 有什么作用?HashMap 的添加、获取时需要通过 key 的 hashCode() 进行 hash(),然后计算下标 ( n-1 & hash),从而获得要找的同的位置。当发生冲突(碰撞)时,利用 key.equals() 方法去链表或树中去查找对应的节点
HashMap 何时扩容?put的元素达到容量乘负载因子的时候,默认16*0.75
hash 的实现吗?h = key.hashCode()) ^ (h >>> 16), hashCode 进行无符号右移 16 位,然后进行按位异或,得到这个键的哈希值,由于哈希表的容量都是 2 的 N 次方,在当前,元素的 hashCode() 在很多时候下低位是相同的,这将导致冲突(碰撞),因此 1.8 以后做了个移位操作:将元素的 hashCode() 和自己右移 16 位后的结果求异或
HashMap线程安全吗?HashMap读写效率较高,但是因为其是非同步的,即读写等操作都是没有锁保护的,所以在多线程场景下是不安全的,容易出现数据不一致的问题,在单线程场景下非常推荐使用。
以上就是HashMap的介绍,希望对你有所收获!
关于作者:mikechen,十余年BAT架构经验,资深技术专家,曾任职阿里、淘宝、百度。
欢迎关注个人公众号:mikechen的互联网架构,十余年BAT架构经验倾囊相授!
在公众号菜单栏对话框回复【架构】关键词,即可查看我原创的300期+BAT架构技术系列文章与1000+大厂面试题答案合集。
HashMap的实现原理(看这篇就够了)的更多相关文章
- 想了解SAW,BAW,FBAR滤波器的原理?看这篇就够了!
想了解SAW,BAW,FBAR滤波器的原理?看这篇就够了! 很多通信系统发展到某种程度都会有小型化的趋势.一方面小型化可以让系统更加轻便和有效,另一方面,日益发展的IC**技术可以用更低的成本生产 ...
- HashMap看这篇就够了
HashMap看这篇就够了 一文读懂HashMap Java8容器源码-目录
- Vue学习看这篇就够
Vue -渐进式JavaScript框架 介绍 vue 中文网 vue github Vue.js 是一套构建用户界面(UI)的渐进式JavaScript框架 库和框架的区别 我们所说的前端框架与库的 ...
- Python GUI之tkinter窗口视窗教程大集合(看这篇就够了) JAVA日志的前世今生 .NET MVC采用SignalR更新在线用户数 C#多线程编程系列(五)- 使用任务并行库 C#多线程编程系列(三)- 线程同步 C#多线程编程系列(二)- 线程基础 C#多线程编程系列(一)- 简介
Python GUI之tkinter窗口视窗教程大集合(看这篇就够了) 一.前言 由于本篇文章较长,所以下面给出内容目录方便跳转阅读,当然也可以用博客页面最右侧的文章目录导航栏进行跳转查阅. 一.前言 ...
- React入门看这篇就够了
摘要: 很多值得了解的细节. 原文:React入门看这篇就够了 作者:Random Fundebug经授权转载,版权归原作者所有. React 背景介绍 React 入门实例教程 React 起源于 ...
- [转]React入门看这篇就够了
摘要: 很多值得了解的细节. 原文:React入门看这篇就够了 作者:Random Fundebug经授权转载,版权归原作者所有. React 背景介绍 React 入门实例教程 React 起源于 ...
- ASP.NET Core WebApi使用Swagger生成api说明文档看这篇就够了
引言 在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必是件很痛苦的事情吧,但文档又必须写,而且文档的格式如果没有具体要求的话,最终完成的文档则完全取决于开发者 ...
- .NET Core实战项目之CMS 第二章 入门篇-快速入门ASP.NET Core看这篇就够了
作者:依乐祝 原文链接:https://www.cnblogs.com/yilezhu/p/9985451.html 本来这篇只是想简单介绍下ASP.NET Core MVC项目的(毕竟要照顾到很多新 ...
- [译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了
[译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了 本文首发自:博客园 文章地址: https://www.cnblogs.com/yilezhu/p/ ...
随机推荐
- 测试开发实战[提测平台]17-Flask&Vue文件上传实现
微信搜索[大奇测试开],关注这个坚持分享测试开发干货的家伙. 先回顾下在此系列第8次分享给出的预期实现的产品原型和需求说明,如下图整体上和前两节实现很相似,只不过一般测试报告要写的内容可能比较多,就多 ...
- CF1569A Balanced Substring 题解
Content 给定一个长度为 \(n\) 且仅包含字符 a.b 的字符串 \(s\).请找出任意一个使得 a.b 数量相等的 \(s\) 的子串并输出其起始位置和终止位置.如果不存在请输出 -1 - ...
- CF111A Petya and Inequiations 题解
Content 请找出一个由 \(n\) 个正整数组成的数列 \(\{a_1,a_2,\dots,a_n\}\),满足以下两种条件: \(\sum\limits_{i=1}^na_i^2\geqsla ...
- Linux 配置与搭建服务
vsftpd nfs autofs samba firewalld selinux lvm 的试验过程 vsftpd 服务端 yum -y install vsftpd echo 'anon_root ...
- C++之递归遍历数组
倒序输出 源码 void print_arr_desc(int arr[], unsigned int len) { if (len) { std::cout << "a[&qu ...
- 1036 - A Refining Company
1036 - A Refining Company PDF (English) Statistics Forum Time Limit: 3 second(s) Memory Limit: 32 ...
- World is Exploding(hdu5792)
World is Exploding Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Other ...
- Docker 与 K8S学习笔记(七)—— 容器的网络
本节我们来看看Docker网络,我们这里主要讨论单机docker上的网络.当docker安装后,会自动在服务器中创建三种网络:none.host和bridge,接下来我们分别了解下这三种网络: $ s ...
- mybatis查询时使用基本数据类型接收报错-attempted to return null from a method with a primitive return type (int)
一.问题由来 自己在查看日志时发现日志中打印了一行错误信息为: 组装已经放养的宠物数据异常--->Mapper method 'applets.user.mapper.xxxMapper.xxx ...
- CS5211|CS5211参数|eDP转LVDS转换器芯片
CS5211概述 CS5211是一个eDP到LVDS转换器,配置灵活,适用于低成本显示系统.CS5211与eDP 1.2兼容,支持1车道和2车道模式,每车道速度为1.62Gbps和2.7Gbps.CS ...