JDK10源码分析之HashMap
HashMap在工作中大量使用,但是具体原理和实现是如何的呢?技术细节是什么?带着很多疑问,我们来看下JDK10源码吧。
1、数据结构
采用Node<K,V>[]数组,其中,Node<K,V>这个类实现Map.Entry<K,V>,是一个链表结构的对象,并且在一定条件下,会将链表结构变为红黑树。所以,JDK10采用的是数组+链表+红黑树的数据结构。贴上Node的源码
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;
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;
}
}
2、静态变量(默认值)
- DEFAULT_INITIAL_CAPACITY= 1 << 4:初始化数组默认长度。1左移4位,为16。
- MAXIMUM_CAPACITY = 1 << 30:初始化默认容量大小,2的30次方。
- DEFAULT_LOAD_FACTOR = 0.75f:负载因子,用于和数组长度相乘,当数组长度大于得到的值后,会进行数组的扩容,扩容倍数是2^n。
- TREEIFY_THRESHOLD = 8:链表长度达到该值后,会进行数据结构转换,变成红黑树,优化速率。
- UNTREEIFY_THRESHOLD = 6:红黑树的数量小于6时,在resize中,会转换成链表。
3、构造函数
/**
* Constructs an empty {@code HashMap} with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
} /**
* Constructs an empty {@code HashMap} with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
} /**
* Constructs an empty {@code HashMap} with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
} /**
* Constructs a new {@code HashMap} with the same mappings as the
* specified {@code Map}. The {@code HashMap} is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified {@code Map}.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
四个构造函数,这里不细说,主要说明一下一个方法。
1、tableSizeFor(initialCapacity)
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
这个方法返回一个2^n的值,用于初始化数组的大小,可以看到,入参的数值不是实际的数组长度,是经过计算得来的大于该值的第一个2^n值,并且,计算后大于2^30时,直接返回2^30。来说明下这个算法的原理,为什么会返回2^n。至于返回2^n有什么用,后面会有说明。
为什么会得到2^n,举个例子。比如13。13的2进制是0000 1101,上面运算相当于以下算式。
0000 1101 右移一位 0000 0110 ,取或0000 1111 一直运算下去,最后+1,确实是2^n。
下面,由于是取或,我们现在只关心二进制最高位的1,后面不管是1或0,都先不看,我们来看以下运算。
000... 1 ... 右移一位与原值取或后,得到 000... 11 ...
000... 11 ... 右移两位与原值取或后,得到 000... 11 11 ...
000... 1111 ... 右移四位与原值取或后,得到 000... 1111 1111 ...
以此下去,在32位范围内的值,通过这样移动后,相当于用最高位的1,将之后的所有值,都补为1,得到一个2^n-1的值。最后+1自然是2^n。
4、主要方法
- put(K key, V value)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//如果数组未初始化,则初始化数组长度
n = (tab = resize()).length;
//计算key的hash值,落在数组的哪一个区间,如果不存在则新建Node元素
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//数组存在的情况下,判断key是否已有,如果存在,则返回该值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果p是红黑树,则直接加入红黑树中
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果不是红黑树,则遍历链表
for (int binCount = 0; ; ++binCount) {
//如果p的next(链表中的下一个值)为空,则直接追加在该值后面
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果该链表存完之后,长度大于8,则转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果next不为空,则比较该链表节点时候就是存入的key,如果是,直接返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果存在相同的key,则直接返回该值。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//数组中元素个数如果大于数组容量*负载因子,则触发数组resize操作。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}HashMap,hash是散列算法,所以HashMap中,主要也用了散列的原理。就是将数据通过hash的散列算法计算其分布情况,存入map中。上面是put的代码,可以看出主要的流程是:初始化一个Node数组,长度为2^n,计算key值落在数组的位置,如果该位置没有Node元素,则用该key建立一个Node插入,如果存在hash碰撞,即不同key计算后的值落在了同一位置,则将该值存到Node链表中。其余具体细节,在上面源码中已经标注。
- hash(key)
计算put的hash入参,源码如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以看到,用到了key的hashCode方法,这个不细说,主要是计算key的散列值。主要讲一下后面为什么要和h右移16后相异或。实际上,是为了让这个hashCode的二进制值的1分布更散列一些。因为后面的运算需要,需要这样做(为什么后面的运算需要让1分散,这个我们下面会讲)。下面我们来看,为什么这样运算后,会增加1的散列性。可以看到,16位以内的二进制hashCode和它右移16位后取异或得到的值是一样的。我们举例时,用8位二进制和它右移4位取异或来举例,
比如 1101 1000 0001 0101,
右移8位为 0000 0000 1101 1000,
取异或后 1101 1000 1100 1101,可以看到1的分布更均匀了一些。
举个极端点的例子 1000 0000 0000 0000
右移8为 0000 0000 1000 0000
取异或后 1000 0000 1000 0000,可以明显看到,1多了一个。所以这样运算是有一定效果的,使hash碰撞的几率要低了一些。
3. resize()
该方法在数组初始化,数组扩容,转换红黑树(treeifyBin中,if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize();)中会触发。主要用于数组长度的扩展2倍,和数据的重新分布。源码如下
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) {
//如果原数组存在,且大于2^30,则设置数组长度为0x7fffffff
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果原数组存在,则将其长度扩展为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);
}
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 { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
4. p = tab[i = (n - 1) & hash]
计算key的hash落在数组的哪个位置,它决定了数组长度为什么是2^n。主要是(n-1) & hash,这里就会用到上面hash()方法中,让1散列的作用。这个方法也决定了,为什么数组长度为2^n,下面我们具体解释一下。由于初始化中,n的值是resize方法返回的,resize中用到的就是tableSizeFor方法返回的2^n的值。如16,下面我举例说明,如数组长度是16:则n-1为15,二进制是 0000 1111与hash取与时,由于0与1/0都为0,所以我们只看后四位1111和hash的后四位。可以看到,与1111取与,可以得到0-15的值,这时,保证了hash能实现落在数组的所有下标。假想一下,如果数组长度为15或其他非二进制值,15-1=14,14的二进制为1110,由于最后一位是0,和任何二进制取与,最后一位都是0,则hash只会落到数组下标为0,2,4,6,8,10,12,14的偶数下标,这样数据分布会更集中,加重每个下标Node的负担,且数组中很多下标无法利用。源码作者正是利用了2^n-1,得到二进制最后全为1,并且与hash相与后,能让hash分布覆盖数组所有下标上的特性。之前hash()方法通过HashCode与HashCode右移16位取异或,让1分布更加均匀,也是为了让hash在数组中的分布更加均匀,从而避免某个下标Node元素过多,效率下降,且过多元素会触发resize耗费时间的缺点,当然,可以看到极端情况下,hash()计算的值并不能解决hash碰撞问题,但是为了HashMap的性能设计者没有考虑该极端情况,也是通过16位hashCode右移8位来举例说明。
如: 1000 1000 0000 0000和1000 1100 0000 0000,如果不移位取异或,这两个hash值与1111取与,都是分布在同一位置,分布情况不良好。
右移8位: 1000 1000 1000 1000和1000 1100 1000 1100,可以看到两个值与1111取与分布在数组的两个下标。
极端情况:1000 0000 0000 0000和1100 0000 0000 0000,该值又移8为取异或后,并不能解决hash碰撞。
JDK10源码分析之HashMap的更多相关文章
- 【集合框架】JDK1.8源码分析之HashMap(一) 转载
[集合框架]JDK1.8源码分析之HashMap(一) 一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化 ...
- 【集合框架】JDK1.8源码分析之HashMap(一)
一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也 ...
- 【集合框架】JDK1.8源码分析之HashMap & LinkedHashMap迭代器(三)
一.前言 在遍历HashMap与LinkedHashMap时,我们通常都会使用到迭代器,而HashMap的迭代器与LinkedHashMap迭代器是如何工作的呢?下面我们来一起分析分析. 二.迭代器继 ...
- JDK1.8源码分析之HashMap(一) (转)
一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也 ...
- 【源码分析】HashMap源码再读-基于Java8
最近工作不是太忙,准备再读读一些源码,想来想去,还是先从JDK的源码读起吧,毕竟很久不去读了,很多东西都生疏了.当然,还是先从炙手可热的HashMap,每次读都会有一些收获.当然,JDK8对HashM ...
- JDK1.8源码分析之HashMap
一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也 ...
- Java高并发程序设计学习笔记(五):JDK并发包(各种同步控制工具的使用、并发容器及典型源码分析(Hashmap等))
转自:https://blog.csdn.net/dataiyangu/article/details/86491786#2__696 1. 各种同步控制工具的使用1.1. ReentrantLock ...
- 【集合框架】JDK1.8源码分析之HashMap
一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也 ...
- java读源码 之 map源码分析(HashMap,图解)一
开篇之前,先说几句题外话,写博客也一年多了,一直没找到一种好的输出方式,博客质量其实也不高,很多时候都是赶着写出来的,最近也思考了很多,以后的博客也会更注重质量,同时也尽量写的不那么生硬,能让大家 ...
随机推荐
- Bootstrap 时间日历插件bootstrap-datetimepicker配置与应用小结
Bootstrap时间日历插件bootstrap-datetimepicker配置与应用小结 by:授客 QQ:1033553122 1. 测试环境 win7 JQuery-3.2.1.min ...
- HTTPS请求处理
HTTPS请求的时候: [1]证书是受信任的,什么都不用做 [2]证书是不受信任的,是自签名的 (1)修改配置文件,禁用ATS特性 (2)信任并安装数字证书 NSURLSession的示例代码如下: ...
- 对于写Java的博文
其实我是有过想要自己总结java相关的知识点给大家分享,但我的Java并不很好,大多数情况我只是拿Java当工具,写一些自己所需一些简单的东西,因此,大多数碰到我不懂的Java问题时,我也是上博客园, ...
- Flutter学习一
包管理 依赖本地包 dependencies: pkg1: path: ../../code/pkg1 依赖git包 dependencies: pkg1: git: url: git://githu ...
- 通过存储过程(SP)实现SQL Server链接服务器(LinkServer)的添加
1. 背景 当系统的微服务化做的不是很高的时候,部分功能要通过DB LinkServer 来实现跨 Server 查询,当然,有时候BI抽数据.DBA数据库维护可能也会创建LinkServer. 特别 ...
- Fiddler应用——使用Fiddler修改指定request/response报文
Fiddler是一个http协议调试代理工具,它能够记录并检查所有你的电脑和互联网之间的http通讯,分析数据,设置断点,修改请求/响应数据,查看所有的“进出”Fiddler的数据(指cookie,h ...
- Druid-代码段-1-5
所属文章:池化技术(一)Druid是如何管理数据库连接的? 本代码段对应流程1.4,抛弃连接: //丢弃连接 public void discardConnection(Connection real ...
- request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+request.getContextPath()+"/"
String path = request.getContextPath(); String basePath = request.getScheme()+"://"+reques ...
- Jupyter notebook 使用
1. 安装代码自动补全 需安装 nbextensions 插件,网站:https://jupyter-contrib-nbextensions.readthedocs.io/en/latest/ins ...
- 程序员,你还不会合理选择Filter、Interceptor、Aspect?
小伙伴们应该听说过过滤器.拦截器.切面,印象上都能够起到截断拦截的作用,在做一些业务需求时,不知道如何选择,今天老顾就来介绍一下他们之间的区别. 过滤器可以拦截到方法的请求和响应 (ServletRe ...