概述

  在上文我们基于JDK7分析了HashMap的实现源码,介绍了HashMap的加载因子loadFactor、阈值threshold概念以及增删元素的机制。JDK8在JDK7的基础上对HashMap的实现进行了进一步的优化,最主要的改变就是新增了红黑树作为底层数据结构。

HashMap数据结构

  首先我们回忆一下JDK7中HashMap的实现,HashMap是以数组和单链表构成,当出现哈希冲突时,冲突的元素在桶中依次形成单链表,数据结构如下:



JDK7中哈希冲突时使用链表存储冲突元素,当出现大量哈希冲突元素,那么在单链表查找一个元素的复杂度为O(N),为了优化出现大量哈希冲突元素的查找问题,JDK8中规定:当单链表存储元素个数超过阈值TREEIFY_THRESHOLD(8)时,将单链表转换为红黑树,红黑树查找元素复杂度为O(logN),提高了查找效率,JDK8中HashMap的存储结构:

内部字段及构造方法

Node类

  使用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;
}
}

TreeNode类

  TreeNode是构成红黑树的基本元素。

	static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red; //构造一个树结点
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}

内部字段

	//数组初始容量,为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //数组最大容量
static final int MAXIMUM_CAPACITY = 1 << 30; //默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f; //单链表转化为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8; /**
* 主要用于resize()扩容过程中, 当对原来的红黑树根据hash值拆分成两条链表后,
* 如果拆分后的链表长度 <=UNTREEIFY_THRESHOLD, 那么就采用链表形式管理hash值冲突;
* 否则, 采用红黑树管理hash值冲突.
*/
static final int UNTREEIFY_THRESHOLD = 6; /**
* 当集合中的容量大于这个值时,表中的桶才能进行树化 ,否则桶内元素太多时会扩容,
* 而不是树形化 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
*/
static final int MIN_TREEIFY_CAPACITY = 64; //第一次使用是初始化,数组长度总是2的幂次
transient Node<K,V>[] table; transient int size; int threshold; final float loadFactor;

构造方法

    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;
//这里只是初始化,最终赋值在resize方法里
this.threshold = tableSizeFor(initialCapacity);
}

哈希值与索引计算

hash(Object key)

  在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

  这个方法非常巧妙,它通过h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。计算过程如下图:

    static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
} //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
//根据hash值和table数组长度计算键值对存储位置的索引
static int indexFor(int h, int length) {
return h & (length-1); //第三步 取模运算
}

存储元素

put(K key, V value)

    public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
} /**
* Implements Map.put and related methods
* @param hash hash for key
* @param key the key 键
* @param value the value to put 值
* @param onlyIfAbsent if true, don't change existing value 表示不要更改现有值
* @param evict if false, the table is in creation mode. false表示table处于创建模式
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//使用局部变量tab而不是类成员,方法栈上访问更快
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果table为null或者长度为0,则进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
//扩容
n = (tab = resize()).length;
//散列到对应的桶中,如果桶为空则直接放到桶中即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//else分支表示散列到的桶中元素不为空
Node<K,V> e; K k;
//桶中链表的根节点的key就是要插入的键值对的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) {
//该Key在链表中不存在,插入末尾 此时e为null
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//单链表元素个数超过TREEIFY_THRESHOLD,树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
//注意从这里break出来的e为null
}
//该Key已经在链表中
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//注意从这里break出来的e不为null
break; p = e;
}
}
// e != null,说明该Key已经在存在于HashMap中,在这个桶中
if (e != null) { // existing mapping for key
V oldValue = e.value;
//根据onlyIfAbsent和old
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//超过阈值,就进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

扩容

resize( )

  resize方法相对比较复杂,但是有比较巧妙,因为要考虑数据结构的不同怎么把元素从老的table中放到扩容后的table中。主要的思路也就是两步:先根据老的table的长度决定扩容后table的长度,以及新的阈值threshold;在桶中数据不为空的情况下,把桶中的数据迁移到新的table数组中,这里就要考虑在桶中只有一个元素(没有发生哈希冲突)、桶中元素以单链表形式存储(发生哈希冲突但是不超过8个)、桶中元素以红黑树形式存储(哈希冲突元素个数超过8个)。只有一个元素,直接根据哈希值和新的table数组长度计算出新的索引,红黑树调用split方法,这里我们重点分析一下怎么把桶中的单链表迁移到新桶中,从而体会到JDK的巧妙设计。

  resize的扩容策略是每次扩容2倍(newThr = oldThr << 1),为了把单链表元素迁移到新的桶中,并不是向JDK7那样直接根据哈希值散列得到新的索引值,经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。



元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:



因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”

    final Node<K,V>[] resize() {
//使用oldTab指向原来的hash表,通常方法内都使用局部变量,局部变量在方法栈上,而对象的成员在堆上
//方法栈的访问比堆更高效
//记录扩容前table数组,阈值,长度
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//原来的容量已经很大了,超过了MAXIMUM_CAPACITY无法再调整
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
}
//oldCap < 0,oldThr > 0,table空间尚未分配,初始化分配空间
//旧阀值大于0,则将新容量直接等于就阀值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//阀值等于0,oldCap也等于0(集合未进行初始化)
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
//threshhold=capacity*load_factor
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;
//原来的表中有内容,表明这是一次扩容,需要将Entry散列到新的位置
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
//遍历所有bin
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//旧桶中元素置为null,方便GC
oldTab[j] = null;
//该桶中只有一个节点,直接散列到新的位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//该桶中是一颗红黑树,通过红黑树的split方法处理
//待会再看split方法
((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;
// 与oldCap按位相与,判断结果是一还是零
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;
}
//原索引+oldCap
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

删除元素

remove(Object key)

    public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
} 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;
//tab不为空,p指向根据hash散列到桶中第一个节点
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;
//第一个节点就命中
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
//在红黑树中查找目标节点
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;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//找到目标节点且符合移除的条件
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);
//单链表移除
//移除的节点是单链表首节点
else if (node == p)
tab[index] = node.next;
//移除的节点不是单链表首节点
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}

查找元素

get(Object key)

    public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
} final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//hash值对应的桶中第一个元素
//第一个元素符合查找条件
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
//红黑树查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//单链表查找
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

总结

  JDK8使用了红黑树优化了HashMap的性能,即使发生了大量的哈希碰撞也能够以O(logN)查找到元素,不会影响到服务器的性能。

JDK源码分析(三)——HashMap 下(基于JDK8)的更多相关文章

  1. JDK源码分析(三)——HashMap 上(基于JDK7)

    目录 HashMap概述 内部字段及构造方法 存储元素 扩容 取出元素 删除元素 判断 总结 HashMap概述   前面我们分析了基于数组实现的ArrayList和基于双向链表实现的LinkedLi ...

  2. JDK源码分析之hashmap就这么简单理解

    一.HashMap概述 HashMap是基于哈希表的Map接口实现,此实现提供所有可选的映射操作,并允许使用null值和null键.HashMap与HashTable的作用大致相同,但是它不是线程安全 ...

  3. 【JDK】JDK源码分析-HashMap(1)

    概述 HashMap 是 Java 开发中最常用的容器类之一,也是面试的常客.它其实就是前文「数据结构与算法笔记(二)」中「散列表」的实现,处理散列冲突用的是“链表法”,并且在 JDK 1.8 做了优 ...

  4. 【JDK】JDK源码分析-HashMap(2)

    前文「JDK源码分析-HashMap(1)」分析了 HashMap 的内部结构和主要方法的实现原理.但是,面试中通常还会问到很多其他的问题,本文简要分析下常见的一些问题. 这里再贴一下 HashMap ...

  5. JDK 源码分析(4)—— HashMap/LinkedHashMap/Hashtable

    JDK 源码分析(4)-- HashMap/LinkedHashMap/Hashtable HashMap HashMap采用的是哈希算法+链表冲突解决,table的大小永远为2次幂,因为在初始化的时 ...

  6. JDK源码分析(三)—— LinkedList

    参考文档 JDK源码分析(4)之 LinkedList 相关

  7. 【集合框架】JDK1.8源码分析之HashMap(一) 转载

    [集合框架]JDK1.8源码分析之HashMap(一)   一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化 ...

  8. JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue

    JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue 目的:本文通过分析JDK源码来对比ArrayBlockingQueue 和LinkedBlocki ...

  9. 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入

    使用react全家桶制作博客后台管理系统   前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...

随机推荐

  1. 开启session

    在index.php中开启 session_start();

  2. HDU 1994 利息计算 数学题

    解题报告:算利息的,不过一开始格式控制符里面少写了一个%lf,一直没看到,愣是没找到错误,唉! #include<cstdio> int main() { int T; scanf(&qu ...

  3. 《区块链100问》第75集:大零币Zcash是什么?

    Zcash,全称Zero Cash,简称ZEC,中文叫大零币,研发者为Zooko Wilcox,诞生于2011年11月9日. 采用零知识证明机制提供完全的支付保密性,是目前匿名性最强的数字资产.零知识 ...

  4. 85.YCbCr与YUV的区别

    yuv色彩模型来源于rgb模型,该模型的特点是将亮度和色度分离开,从而适合于图像处理领域. YCbCr模型来源于yuv模型,应用于数字视频,ITU-R BT.601 recommendation 通过 ...

  5. innobackupex 相关语法讲解【转】

    innobackupex 相关语法讲解 连接服务器 The database user used to connect to the server and its password are speci ...

  6. BigDecimal常用方法

    一.介绍 Java中提供了大数字(超过16位有效位)的操作类,即 java.math.BinInteger 类和 java.math.BigDecimal 类,用于高精度计算. 其中 BigInteg ...

  7. day04作业

    1.for(初始化表达式:条件表达式:循环后的操作表达式){ 循环体: } class Test_Sum { public static void main(String[] args) { int ...

  8. ExtJs对js基本语法扩展支持

    ExtJs对js基本语法扩展支持 本篇主要介绍一下ExtJs对JS基本语法的扩展支持,包括动态加载.类的封装等. 一.动态引用加载 ExtJs有庞大的类型库,很多类可能在当前的页面根本不会用到,我们可 ...

  9. 5 个非常有用的 Laravel Blade 指令,你用过哪些?

    接下来我将带大家认识下五个 Laravel Blade 指令,这些指令将让你在解决特定问题时如虎添翼.如果你是刚接触 Laravel 的用户,这些小技巧能带你认识到 Laravel Blade 模板引 ...

  10. HDU 5115 Dire Wolf (区间DP)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=5115 题目大意:有一些狼,从左到右排列,每只狼有一个伤害A,还有一个伤害B.杀死一只狼的时候,会受到这 ...