Java源码系列2——HashMap
HashMap 的源码很多也很复杂,本文只是摘取简单常用的部分代码进行分析。能力有限,欢迎指正。
HASH 值的计算
前置知识——位运算
按位异或操作符^:1^1=0, 0^0=0, 1^0=0, 值相同为0,值不同为1。按位异或就是对二进制中的每一位进行异或运算。
1111 0000 1111 1110
^ 1111 1111 0000 1111
______________________
0000 1111 1111 0001
按位右移补零操作符>>> :左操作数按右操作数指定的位数右移,移动得到的空位以零填充。
1110 1101 1001 1111
>>> 4
___________________________
0000 1110 1101 1001
扰动函数
为什么要做扰动?
理论上哈希值是一个int类型,如果直接拿哈希值做下标的话,考虑到2进制32位带符号的int表值范围从-2147483648到2147483648。前后加起来大概40亿的映射空间。这么大的数组,内存是存不下的,所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来访问数组下标。
因为只取最后几位,所以哈希碰撞的可能性大大增加,这时候扰动函数的价值就来了。
扰动计算

先调用hashCode()方法得出hash值,再进行扰动操作。
右位移16位,正好是32bit的一半(int 是32位的),自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也变相保留下来。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
取模,计算出下标
在计算下标的时候,让列表长度对哈希值做取模操作,让计算出来的哈希值在列表范围内,n 为list长度
i = (n - 1) & hash
为什么HashMap的数组长度要取2的整次幂
因为这样(数组长度 - 1)正好相当于一个“低位掩码”。&操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组的下标访问。以初始长度16为例,16-1=15,2进制表示是0000 1111。和某散列值做&操作如下:
1010 0011 0110 1111 0101
& 0000 0000 0000 0000 1111
____________________________
0000 0000 0000 0000 0101
是什么存入了 table
HashMap存入table的值并不只有value,而是构造成一个 Node 对象实例存入 table。
Node对象里有:hash, key, value, next(哈希冲突时的链表)
理论最大容量
int MAXIMUM_CAPACITY = 1 << 30;
2的30次方
负载因子
负载因子是用来计算负载容量(所能容纳的最大Node个数)的,当前list长度 length,负载因子 loadFactor
负载容量计算公式为:
threshold = length * loadFactor
默认负载因子为 0.75。也就是说,当Node个数达到当前list长度的75%时,就要进行扩容,否则会增加哈希碰撞的可能性。负载因子的作用是在空间和时间效率上取得一个平衡。
float DEFAULT_LOAD_FACTOR = 0.75f
扩容做了哪些操作
- 创建一个新的Entry空数组,长度是原数组的2倍。
当Node个数超过负载容量时,进行扩容。
old << 1 左移一位相当于 old * 2。
重新Hash
遍历原Entry数组,把所有的Entry重新Hash到新数组中。
为什么要重新hash?因为长度扩大以后,hash值也随之改变(数组下标的计算是数组长度对hashcode进行取模)。
这样就可以把原先哈希冲突的链表拉平,使数组变得稀疏。
final Node<K,V>[] resize() {
// 保存现有的数组
Node<K,V>[] oldTab = table;
// 保存现有的数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 保存现有的负载容量
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 如果现有容量已经超过最大值了,就没办扩容了,只好随你碰撞了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 原有容量左移一位,相当于 oldCap * 2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 负载容量也扩大一倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 负载容量为0,根据数组大小和负载因子计算出来
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 遍历数组中所有元素,重新进行hash
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
// 删除旧索引位置的值
oldTab[j] = null;
if (e.next == null)
// 给新的索引位置赋值
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 优化链表
// 把原有链表拆成两个链表
// 链表1存放在低位(原索引位置)
Node<K,V> loHead = null, loTail = null;
// 链表2存放在高位(原索引 + 旧数组长度)
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 链表1
// 这个位运算的原理可以参考第三篇参考资料
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 链表2
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 链表1存放于原索引位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 链表2存放原索引加上旧数组长度的偏移量
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
树化改造
链表长度太长,会被改造成红黑树。
当链表的长度超过MIN_TREEIFY_CAPACITY 最大树化临界值,就会进行树化改造。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
...
}
}
为什么要树化?
本质上是个安全问题。因为链表查询影响性能,如果有人恶意造成哈希碰撞,就会构成哈希碰撞拒绝服务攻击,服务端CPU被大量占用用于链表查询,造成服务变慢或不可用。
源码系列文章
参考
JDK 源码中 HashMap 的 hash 方法原理是什么?胖君的回答
本文首发于我的个人博客 http://chaohang.top
作者张小超
转载请注明出处
欢迎关注我的微信公众号 【超超不会飞】,获取第一时间的更新。

Java源码系列2——HashMap的更多相关文章
- Java源码系列4——HashMap扩容时究竟对链表和红黑树做了什么?
我们知道 HashMap 的底层是由数组,链表,红黑树组成的,在 HashMap 做扩容操作时,除了把数组容量扩大为原来的两倍外,还会对所有元素重新计算 hash 值,因为长度扩大以后,hash值也随 ...
- Java源码系列1——ArrayList
本文简单介绍了 ArrayList,并对扩容,添加,删除操作的源代码做分析.能力有限,欢迎指正. ArrayList是什么? ArrayList 就是数组列表,主要用来装载数据.底层实现是数组 Obj ...
- 【java集合框架源码剖析系列】java源码剖析之HashMap
前言:之所以打算写java集合框架源码剖析系列博客是因为自己反思了一下阿里内推一面的失败(估计没过,因为写此博客已距阿里巴巴一面一个星期),当时面试完之后感觉自己回答的挺好的,而且据面试官最后说的这几 ...
- Java源码解读(一)——HashMap
HashMap作为常用的一种数据结构,阅读源码去了解其底层的实现是十分有必要的.在这里也分享自己阅读源码遇到的困难以及自己的思考. HashMap的源码介绍已经有许许多多的博客,这里只记录了一些我看源 ...
- Java 源码刨析 - HashMap 底层实现原理是什么?JDK8 做了哪些优化?
[基本结构] 在 JDK 1.7 中 HashMap 是以数组加链表的形式组成的: JDK 1.8 之后新增了红黑树的组成结构,当链表大于 8 并且容量大于 64 时,链表结构会转换成红黑树结构,它的 ...
- [java源码解析]对HashMap源码的分析(二)
上文我们讲了HashMap那骚骚的逻辑结构,这一篇我们来吹吹它的实现思想,也就是算法层面.有兴趣看下或者回顾上一篇HashMap逻辑层面的,可以看下HashMap源码解析(一).使用了哈希表得“拉链法 ...
- [java源码解析]对HashMap源码的分析(一)
最近有空的时候研究了下HashMap的源码,平时我用HashMap主要拿来当业务数据整理后的容器,一直觉得它比较灵活和好用, 这样 的便利性跟它的组成结构有很大的关系. 直接开门见山,先简要说明一下H ...
- 【java集合框架源码剖析系列】java源码剖析之HashSet
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本.本博客将从源码角度带领大家学习关于HashSet的知识. 一HashSet的定义: public class HashSet&l ...
- JAVA源码分析-HashMap源码分析(一)
一直以来,HashMap就是Java面试过程中的常客,不管是刚毕业的,还是工作了好多年的同学,在Java面试过程中,经常会被问到HashMap相关的一些问题,而且每次面试都被问到一些自己平时没有注意的 ...
随机推荐
- mybatis 执行流程以及初用错误总结
mappper 配置文件 头文件: 1. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" &q ...
- Leetcode 题目整理 climbing stairs
You are climbing a stair case. It takes n steps to reach to the top. Each time you can either climb ...
- sql serverDB运维实用sql大全
运维sql server的sql总结,包含阻塞语句.等待语句.某个时间段的sql性能查询等等常用sql语句 ##断开库的连接,记得修改库名 USE masterGOALTER DATABASE [DB ...
- 乌班图14更新软件提示错误:https://mirrors.aliyun.com kubernetes-xenial InRelease: 由于没有公钥,无法验证下列签名: NO_PUBKEY 6A030B21BA07F4FB
提示如下 获取: https://mirrors.aliyun.com kubernetes-xenial InRelease 忽略 https://mirrors.aliyun.com kubern ...
- 用java编写代码实现关机
public static void main(String[] args) { Runtime runtime = Runtime.getRuntime(); try { runtime.exec( ...
- QtGui实现计算圆的面积
dialog.h #ifndef DIALOG_H #define DIALOG_H #include <QtWidgets/QDialog> #include <QtWidgets ...
- Pycharm2019.1.1永久破解
声明:不做商业用途,电脑系统win10专业版,亲测有效. 1,下载新版破解补丁+安装包 网盘链接 提取码:hn0u 下载补丁文件 jetbrains-agent.jar 并将它放置到 pycharm安 ...
- Java并发读书笔记:Lock与ReentrantLock
Lock位于java.util.concurrent.locks包下,是一种线程同步机制,就像synchronized块一样.但是,Lock比synchronized块更灵活.更复杂. 话不多说,我们 ...
- Yandex Big Data Essentials Week1 Unix Command Line Interface File System exploration
File System Function In computing, a file system or filesystem is used to control how data is stored ...
- 安装ik分词插件
下载IK安装包 https://github.com/medcl/elasticsearch-analysis-ik https://github.com/medcl/elasticsearch-an ...