前言:上篇文章,笔者分析了jdk1.7中HashMap的源码,这里将对jdk1.8的HashMap的源码进行分析。

注:本文jdk源码版本为jdk1.8.0_172


1.再看put操作

  public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

jdk1.8中的hash算法:

  static final int hash(Object key) {
int h;
// 这里的hash算法和jdk1.7中很不一样,直接高16位与低16位做异或,这样的话高低位都进行了运算,增加hash值的随机性
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

再看put操作的核心函数:

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// jdk1.8中HashMap底层数据结构使用的是Node
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果table还未初始化,则初始化table,注意这里初始化使用的是resize函数[扩容函数]
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/**
* 这里表示如果tab[i]位置上为null,则直接插入数据
* i=(n-1)&hash与jdk1.7中找出元素在tab上的index是一样的操作
* 注意这里在多线程环境下会造成线程不安全问题
*/
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {// 如果i位置上有元素,则进行链式存储
Node<K,V> e; K k;
// 如果tab[i]上的元素与插入元素的key完全一样,则进行覆盖操作
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = 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) {
// 从这里可知道jdk1.8在,如果存在链表,插入数据是直接放在链表尾的
p.next = newNode(hash, key, value, null);
// 当同一节点链表中元素个数>=8时,底层数据结构转向红黑树,
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash); // 将底层数据结构转向红黑树
break;
}
// 判断next元素是否和插入元素相同,如果相同,则不做操作,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; // 将next赋值给p,继续循环
}
}
// 覆盖操作
if (e != null) { // existing mapping for key
V oldValue = e.value;
// onlyIfAbsent表示是否要改变原来的值,true-不改变,false-改变
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 修改次数加1,fail-fast机制
++modCount;
// 判断是否需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

重点:

jdk1.8中HashMap在进行put操作时:

#1.如果同一hash值的节点数小于8时,底层数据结构仍然是链表;当节点数大于等于8时,会转向红黑树。

#2.如果节点的数据结构是链表时,插入数据是直接放在链表尾的,从而避免插入元素时,形成环形链,造成死循环。

put操作的核心代码中,还涉及一个比较重要的函数,这里进行详细分析。

#resize()

 final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 如果table中有元素
if (oldCap > 0) {
// 容量是否已达限制
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 扩容,并更新扩容阈值
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 如果table中没有元素,但是已初始化扩容阈值,这里将table的新容量赋值为扩容阈值
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;
// 如果原来table有值,则循环将原值转移到newTab中
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 找到有值的节点
if ((e = oldTab[j]) != null) {
oldTab[j] = null; //将原来table中当前位置置null
if (e.next == null) // 如果当前节点next为null,将其放置在newTab中的新位置
newTab[e.hash & (newCap - 1)] = e;
// 如果是红黑树则进行红黑树操作,关于红黑树后面会进行分析
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order // 当走到这里,说明节点上为链表形式存储数据,需进行循环操作
// 存储位置在newTable和oldTable位置不变元素
Node<K,V> loHead = null, loTail = null;
// 存储oldTable中位置发生了变化的元素,当然这里是和oldTable相比较
// 参看下面的注释,应该可以很好理解
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
// 由于是链表循环,因此需存储next节点的值,这种形式在jdk1.7中出现过多次
next = e.next;
/**
*这里需要注意一下,这里是用元素的hash值,与原来table长度做&操作
* 如果为0,则表示e.hash&(newCap-1)和e.hash&(oldCap-1)是一样的
* 也就说元素的位置在newTable中是不变的,因为newTable的大小为oldTable大小的2倍
* 相当于其二进制向左移动了1位,其newCap-1的二进制全都为1,且比原来oldCap-1的二进制多了一个1
* eg:oldCap=16,newCap=32,注意求key的位置是用e.hash&(table.length-1)
* e.hash&0x1111=原来key的位置
* e.hash&0x10000=0,表明e.hash在二进制的第5位上一定为0,所以:
* e.hash&0x11111=也一定是原来key的位置
* 如果:
* e.hash&0x10000=1,表明e.hash在二进制的第5位上一定为1,所以:
* e.hash&0x11111=原来key的位置加上oldCap的长度即可(0x10000)
* 这样根据一个二进制位就将原来的一条链表分成两条链表进行存储,这里非常的关键,不是很好理解
* 仔细理解上面的解释,相信你会发现这是非常神奇的一个技巧
*/
// 有了上面的原理,再来看这就非常明确了
// 元素在newTable中位置不改变
if ((e.hash & oldCap) == 0) {
// 初始时,将e放在loHead头上,然后尾又是e,后续循环的时候,只操作tail就行了,形成链表
if (loTail == null)
loHead = e;
else
loTail.next = e;
// 尾部存储为e,形成链表,注意理解就好
loTail = e;
}
// 元素在newTable中位置发生了变化[相对oldTable]
// 这里就相当于两条链表了,位置不变的一条,位置变了的又是一条
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 如果位置不变链表不为null
if (loTail != null) {
loTail.next = null;
// 从这里也可看出这里存储的是元素在newTable中位置不改变[相对oldTable]
// 只需要存储head值即可,因为已形成链表
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
// 位置变化的元素,位置只需要加上oldCap的值就可以了,上面已进行分析
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

重点:

resize函数中在对节点元素存在链表时的处理有点小技巧,虽然是再次hash,但它是根据key在oldTable中位置与newTable的位置来进行区分的,变成两条链表存储,具体分析过程在函数中已注释写得非常详细。

jdk1.8中HashMap主要在底层增加了红黑树的数据结构,关于红黑树这种数据结构笔者还是有点懵懂,后面会专门深入这方面的知识点。

2.再看get操作

 public V get(Object key) {
// 这里与jdk1.7的get方法有很大的不一样
Node<K,V> e;
// 通过getNode方法,如果e不为null,则返回e的value,否则返回null
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

get关键函数getNode:

 final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 如果HashMap中有值,其当前元素在table中有值,则进行寻找
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 注释已经说的非常清楚了,检查第一个节点的值是否与要查找的相同,快速return
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果当前节点下存在红黑树或链表结构,则进行循环操作
if ((e = first.next) != null) {
// 如果next节点为红黑树则在红黑树中查找元素
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 走到该分支,则表明当前节点下为链表结构,下面的循环就比较简单了,通过循环不断的查找相同的元素,有则返回,无则返回null
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 否则直接返回null
return null;
}

分析:

#1.get函数的核心代码逻辑还是非常简单的,注意:总是首先校验头结点,快速return。

#2.其次是红黑树中查找元素,由于红黑树的数据结构相对较复杂,后续在进行相应分析。

3.其他重要函数

#removeNode该函数为remove函数的核心函数:

 /**
* HashMap删除元素
* @param hash key的hash值
* @param key 传入的删除的key值
* @param value 值,传入为nul
* @param matchValue if true only remove if value is equal 如果为true,则只移除value相等的元素,所以这里传入false
* @param movable if false do not move other nodes while removing 该值主要用于红黑树移除节点后,再重塑红黑树,所以传入true
* @return
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 同样先判断是否存在该元素,并记录头结点元素p
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 如果头结点直接命中,则用node存储下来
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 该分支表示头结点未命中,判断p结点的next是否不为null,因为要循环,所以需要将next元素记录下来,这种方式在HashMap的循环中很常用
else if ((e = p.next) != null) {
// 如果p为红黑树结构,则走红黑树分支,找到要删除的结点
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
// 否则进行循环查找
else {
do {
// 如果命中,则跳出循环
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e; // node结点保存命中的结点元素
break;
}
// 如果未命中,则用p记录next结点,为后续删除做准备,p表示要删除节点的前一个节点,因为这里有e=e.next操作,与jdk1.7相同。
p = e;
} while ((e = e.next) != null);
}
}
// 要删除结点不为null,&& 操作后:注意这里!matchValue,因为传入值为false,所以这里一直为true
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 如果要删除节点为红黑树,则走红黑树分支
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 如果要删除节点与p相同,则说明是头结点,则直接将tab[index]位置指向node.next,这样就踢出了node元素,即删除
else if (node == p)
tab[index] = node.next;
else // 如果不相同,则直接将p.next指向node.next,同样跳过了node,也就删除了。
p.next = node.next;
++modCount; //操作记录+1,fail-fast机制
--size;// 元素个数减1
afterNodeRemoval(node);
return node;
}
}
// 上述都未找到,则直接返回null
return null;
}

重点:

删除函数与jdk1.7中原理一样,都是通过操作一个结点元素进行删除,只是如果结点为红黑树,需走红黑树分支。

总结

这里比较粗略的分析了jdk1.8中HashMap的源码,它与jdk1.7中源码的原理大致相同,这里总结其重点:

#1.底层引入了红黑树数据结构,在添加元素时,如果table位置上的元素数量>=8时,则当前位置结点数据结构会转向红黑树;

当table位置上元素数量<=6时,数据结构又会转换成链表(在resize中,红黑树分支)。

#2.改变了hash算法,直接高16位与低16位做异或。

#3.resize函数中,在做再次hash时,用两条链表分散存储节点,并且避免了jdk1.7中的死循环情况。

#4.同样存在fail-fast机制。


by Shawn Chen,2019.03.09,晚。

HashMap源码分析(二)的更多相关文章

  1. HashMap源码分析(二):看完彻底了解HashMap

    上文讲到HashMap的增加方法,现在继续 上文链接 HashMap在上一篇源码分析的文章中,如果使用put的时候如果元素数量超过threshold就会调用resize进行扩容 1.扩容机制 想要了解 ...

  2. JAVA源码分析-HashMap源码分析(二)

    本文继续分析HashMap的源码.本文的重点是resize()方法和HashMap中其他的一些方法,希望各位提出宝贵的意见. 话不多说,咱们上源码. final Node<K,V>[] r ...

  3. HashMap源码分析二

    jdk1.2中HashMap的源码和jdk1.3中HashMap的源码基本上没变.在上篇中,我纠结的那个11和101的问题,在这边中找到答案了.   jdk1.2   public HashMap() ...

  4. 【JAVA集合】HashMap源码分析(转载)

    原文出处:http://www.cnblogs.com/chenpi/p/5280304.html 以下内容基于jdk1.7.0_79源码: 什么是HashMap 基于哈希表的一个Map接口实现,存储 ...

  5. Java中HashMap源码分析

    一.HashMap概述 HashMap基于哈希表的Map接口的实现.此实现提供所有可选的映射操作,并允许使用null值和null键.(除了不同步和允许使用null之外,HashMap类与Hashtab ...

  6. JDK1.8 HashMap源码分析

      一.HashMap概述 在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里.但是当位于一个桶中的元素较多,即hash值相等的元素较多时 ...

  7. 【Java】HashMap源码分析——常用方法详解

    上一篇介绍了HashMap的基本概念,这一篇着重介绍HasHMap中的一些常用方法:put()get()**resize()** 首先介绍resize()这个方法,在我看来这是HashMap中一个非常 ...

  8. 【Java】HashMap源码分析——基本概念

    在JDK1.8后,对HashMap源码进行了更改,引入了红黑树.在这之前,HashMap实际上就是就是数组+链表的结构,由于HashMap是一张哈希表,其会产生哈希冲突,为了解决哈希冲突,HashMa ...

  9. Java BAT大型公司面试必考技能视频-1.HashMap源码分析与实现

    视频通过以下四个方面介绍了HASHMAP的内容 一. 什么是HashMap Hash散列将一个任意的长度通过某种算法(Hash函数算法)转换成一个固定的值. MAP:地图 x,y 存储 总结:通过HA ...

  10. Java源码解析——集合框架(五)——HashMap源码分析

    HashMap源码分析 HashMap的底层实现是面试中问到最多的,其原理也更加复杂,涉及的知识也越多,在项目中的使用也最多.因此清晰分析出其底层源码对于深刻理解其实现有重要的意义,jdk1.8之后其 ...

随机推荐

  1. linux 进程概念

    1,pcb:进程控制块结构体:/usr/src/linux-headers-4.15.0-29/include/linux/sched.h 进程id:系统中每个进程有唯一的id,在c语言中用pid_t ...

  2. July 03rd. 2018, Week 27th. Tuesday

    I don't know anything with certainty, but seeing the stars makes me dream. 我不知道世间有什么事是确定不变的,但只要一看到星空 ...

  3. 快速构建H5单页面切换应用

    在Web App和Hybrid App横行的时代,为了拥有更好的用户体验,单页面应用顺势而生,单页面应用简称`SPA`,即Single Page Application,就是只有一个HTML页面的应用 ...

  4. PHP面向对象特性

    目录 创建对象 成员属性 成员方法 构造方法 析构方法 垃圾回收机制 访问修饰符 魔术方法 对象比较 继承 重载 属性重载 方法重写 属性重写 静态属性 静态方法 多态 类型约束 抽象类 接口 fin ...

  5. web服务器之nginx和apache的区别

    ① apache属于重量级的服务器,nginx属于轻量级的服务器; 区别在于对一些功能的支持,比如:  pathinfo,php模块方面 ② nginx抗高并发能力强. 由于nginx采用的是异步非阻 ...

  6. Google File System 见解 (作业)

    Google File System ——见解 近年来,大街小巷都传遍的大数据,引起了社会的一阵学习大数据狂热,造成任何公司在招聘人员的时候都会注上一条,会大数据的优先考虑:但是,从另一方面来说,这狂 ...

  7. ACache【轻量级的开源缓存框架】

    版权声明:本文为HaiyuKing原创文章,转载请注明出处! 前言 官方介绍 ASimpleCache 是一个为android制定的 轻量级的 开源缓存框架.轻量到只有一个java文件(由十几个类精简 ...

  8. SpringBoot + Spring Security 学习笔记(二)安全认证流程源码详解

    用户认证流程 UsernamePasswordAuthenticationFilter 我们直接来看UsernamePasswordAuthenticationFilter类, public clas ...

  9. 【JVM系列】一步步解析java执行内幕

    对于任何一门语言,要想达到精通的水平,研究它的执行原理(或者叫底层机制)不失为一种良好的方式.在本篇文章中,将重点研究java源代码的执行原理,即从程 序员编写JAVA源代码,到最终形成产品,在整个过 ...

  10. 极光推送经验之谈-Java后台服务器实现极光推送的两种实现方式

    原创作品,可以转载,但是请标注出处地址http://www.cnblogs.com/V1haoge/p/6439313.html Java后台实现极光推送有两种方式,一种是使用极光推送官方提供的推送请 ...