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,图解)一
开篇之前,先说几句题外话,写博客也一年多了,一直没找到一种好的输出方式,博客质量其实也不高,很多时候都是赶着写出来的,最近也思考了很多,以后的博客也会更注重质量,同时也尽量写的不那么生硬,能让大家 ...
随机推荐
- JPA的基本注解
场景 JPA入门简介与搭建HelloWorld(附代码下载): https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/103473937 ...
- SpringBoot+MyBatisPlus+ElementUI一步一步搭建前后端分离的项目(附代码下载)
场景 一步一步教你在IEDA中快速搭建SpringBoot项目: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/87688277 ...
- python中线程 进程 协程
多线程:#线程的并发是利用cpu上下文的切换(是并发,不是并行)#多线程执行的顺序是无序的#多线程共享全局变量#线程是继承在进程里的,没有进程就没有线程#GIL全局解释器锁#只要在进行耗时的IO操作的 ...
- java8-14-时间API
原来的时间类 1.默认值 我们使用起来不方便 2.在不同包 不规范 在java.util和java.sql的包中都有日期类,此外用于格式化和解析的类在java.text包中定义 3.可变 线程 ...
- Bert镜像制作及flask生产环境模式启动
一天搞定两大技术点,成就满满. 一,dockerfile FROM harbor.xxx.com.cn/3rd_part/tensorflow:1.14.0-gpu-py3-jupyter LABEL ...
- Re-爬楼梯
题目地址 https://dn.jarvisoj.com/challengefiles/CFF_100.rar.dbeee1536c0a5ef5844f42c93602aae5 看看功能,看样子要爬到 ...
- 信号驱动式I/O
信号驱动式I/O是指进程预先告知内核,使得当某个描述符上发生某事时,内核使用信号通知相关进程. 异步I/O是进程执行I/O系统调用(读或写)告知内核启动某个I/O操作,内核启动I/O操作后立刻返回到进 ...
- 2015 经典的ImageCaptioning论文
1.Show and Tell: A Neural Image Caption Generator Google团队的成果 整体处理流程: 1)通过CNN提取到图片的特征,简称feature. 2)而 ...
- 漫长的 windows caffe编译过程 (OnlyCpu)
在windows下 编译caffe代码. 官方推荐是vs2013 而我手头只有VS2017 .准备开始下载代码开始编译. 1 代码下载 windows版本的代码有两份,一份是官方的编译的windows ...
- LG4516/LOJ2546 「JSOI2018」潜入行动 树上背包
问题描述 LG4516 LOJ2546 题解 好一个毒瘤题. hkk:JSOI的签到题 设\(opt[i][j][0/1][0/1]\)代表结点\(i\)的子树,放置\(j\)个,\(i\)放不放,\ ...