Java HashMap底层实现原理源码分析Jdk8
在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,可能会将链表转换为红黑树,这样大大减少了查找时间。
简单说下HashMap的实现原理:
首先存在一个table数组,里面每个元素都是一个node链表,当添加一个元素(key-value)时,就首先计算元素key的hash值,通过table的长度和key的hash值进行与运算得到一个index,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就把这个元素添加到同一hash值的node链表的链尾,他们在数组的同一位置,但是形成了链表,同一各链表上的Hash值是相同的,所以说数组存放的是链表。而当链表长度大于等于8时,链表就可能转换为红黑树,这样大大提高了查找的效率。
存储结构
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;
}
*
*
*
}
transient Node<K,V>[] table;
- HashMap内部包含一个Node类型的数组table,Node由Map.Entry继承而来。
- Node存储着键值对。它包含四个字段,从next字段我们可以看出node是一个链表。
- table数组中的每个位置都可以当做一个桶,一个桶存放一个链表。
- HashMap使用拉链法来解决冲突,同一个存放散列值相同的Node。
数据域
// 序列化ID
private static final long serialVersionUID = 362498820763181265L;
// 初始化容量,初始化有16个桶
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量 1 073 741 824, 10亿多
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的负载因子。因此初始情况下,当键值对的数量大于 16 * 0.75 = 12 时,就会触发扩容。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当put()一个元素到某个桶,其链表长度达到8时有可能将链表转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 在hashMap扩容时,如果发现链表长度小于等于6,则会由红黑树重新退化为链表。
static final int UNTREEIFY_THRESHOLD = 6;
// 在转变成红黑树树之前,还会有一次判断,只有键值对数量大于 64 才会发生转换,否者直接扩容。这是为了避免在HashMap建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的转化。
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组
transient Node<k,v>[] table;
// 存放元素的个数
transient int size;
// 被修改的次数fast-fail机制
transient int modCount;
// 临界值 当实际大小(容量*填充比)超过临界值时,会进行扩容
int threshold;
// 填充比
final float loadFactor;
构造函数
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
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;
// tableSizeFor(initialCapacity)方法计算出接近initialCapacity
// 参数的2^n来作为初始化容量。
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
- HashMap构造函数允许用户传入容量不是2的n次方,因为它可以自动地将传入的容量转换为2的n次方。
put()操作源码解析
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
// “扰动函数”。参考 https://www.cnblogs.com/zhengwang/p/8136164.html
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
// 未初始化则初始化table
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 通过table的长度和hash与运算得到一个index,
// 然后判断table数组下标为index处是否已经存在node。
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果table数组下标为index处为空则新创建一个node放在该处
tab[i] = newNode(hash, key, value, null);
else {
// 运行到这代表table数组下标为index处已经存在node,即发生了碰撞
HashMap.Node<K,V> e; K k;
// 检查这个node的key是否跟插入的key是否相同。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 检查这个node是否已经是一个红黑树
else if (p instanceof TreeNode)
// 如果这个node已经是一个红黑树则继续往树种添加节点
e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
// 在这里循环遍历node链表
// 判断是否到达链表尾
if ((e = p.next) == null) {
// 到达链表尾,直接把新node插入链表,插入链表尾部,在jdk8之前是头插法
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 如果node链表的长度大于等于8则可能把这个node转换为红黑树
treeifyBin(tab, hash);
break;
}
// 检查这个node的key是否跟插入的key是否相同。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 当插入key存在,则更新value值并返回旧value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 修改次数++
++modCount;
// 如果当前大小大于门限,门限原本是初始容量*0.75
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
- 下面简单说下put()流程:
- 判断键值对数组table[]是否为空或为null,否则以默认大小resize();
- 根据键key计算hash值与table的长度进行与运算得到插入的数组索引 index,如果tab[index] == null,直接根据key-value新建node添加,否则转入3
- 判断当前数组中处理hash冲突的方式为链表还是红黑树(check第一个节点类型即可),分别处理
- 为啥头插法为什么要换成尾插:jdk1.7时候用头插法可能是考虑到了一个所谓的热点数据的点(新插入的数据可能会更早用到);找到链表尾部的时间复杂度是 O(n),或者需要使用额外的内存地址来保存链表尾部的位置,头插法可以节省插入耗时。但是在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。
- 从putVal()源码可以看出,HashMap并没有对null的键值对做限制(hash值设为0),即HashMap允许插入键尾null的键值对。但在JDK1.8之前HashMap使用第0个node存放键为null的键值对。
- 确定node下标:通过table的长度和key的hash进行与运算得到一个index。
- 在转变成红黑树树之前,还会有一次判断,只有键值对数量大于 64 才会发生转换,否者直接扩容。这是为了避免在HashMap建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的转化。
get()操作源码解析
public V get(Object key) {
HashMap.Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final HashMap.Node<K,V> getNode(int hash, Object key) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k;
// table不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
// 通过table的长度和hash与运算得到一个index,table
// 下标位index处的元素不为空,即元素为node链表
(first = tab[(n - 1) & hash]) != null) {
// 首先判断node链表中中第一个节点
if (first.hash == hash && // always check first node
// 分别判断key为null和key不为null的情况
((k = first.key) == key || (key != null && key.equals(k))))
// key相等则返回第一个
return first;
// 第一个节点key不同且node链表不止包含一个节点
if ((e = first.next) != null) {
// 判断node链表是否转为红黑树。
if (first instanceof HashMap.TreeNode)
// 则在红黑树中进行查找。
return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 循环遍历node链表中的节点,判断key是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// key在table中不存在则返回null。
return null;
}
- get(key)方法首先获取key的hash值,
- 计算hash & (table.len - 1)得到在链表数组中的位置,
- 先判断node链表(桶)中的第一个节点的key是否与参数key相等,
- 不等则判断是否已经转为红黑树,若转为红黑树则在红黑树中查找,
- 如没有转为红黑树就遍历后面的链表找到相同的key值返回对应的Value值即可。
resize()操作源码解析
// 初始化或者扩容之后的元素调整
final HashMap.Node<K,V>[] resize() {
// 获取旧table
HashMap.Node<K,V>[] oldTab = table;
// 旧table容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 旧table扩容临界值
int oldThr = threshold;
// 定义新table容量和临界值
int newCap, newThr = 0;
// 如果原table不为空
if (oldCap > 0) {
// 如果table容量达到最大值,则修改临界值为Integer.MAX_VALUE
// MAXIMUM_CAPACITY = 1 << 30;
// Integer.MAX_VALUE = 1 << 31 - 1;
if (oldCap >= MAXIMUM_CAPACITY) {
// Map达到最大容量,这时还要向map中放数据,则直接设置临界值为整数的最大值
// 在容量没有达到最大值之前不会再resize。
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
/*
* 进入此if证明创建HashMap时用的带参构造:public HashMap(int initialCapacity)
* 或 public HashMap(int initialCapacity, float loadFactor)
* 注:带参的构造中initialCapacity(初始容量值)不管是输入几都会通过
* tableSizeFor(initialCapacity)方法计算出接近initialCapacity
* 参数的2^n来作为初始化容量。
* 所以实际创建的容量并不等于设置的初始容量。
*/
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 进入此if证明创建map时用的无参构造:
// 然后将参数newCap(新的容量)、newThr(新的扩容阀界值)进行初始化
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
// 进入这代表有两种可能。
// 1. 说明old table容量大于0但是小于16.
// 2. 创建HashMap时用的带参构造,根据loadFactor计算临界值。
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 修改临界值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 根据新的容量生成新的 table
HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
// 替换成新的table
table = newTab;
// 如果oldTab不为null说明是扩容,否则直接返回newTab
if (oldTab != null) {
/* 遍历原来的table */
for (int j = 0; j < oldCap; ++j) {
HashMap.Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 判断这个桶(链表)中就只有一个节点
if (e.next == null)
// 根据新的容量重新计算在table中的位置index,并把当前元素赋值给他。
newTab[e.hash & (newCap - 1)] = e;
// 判断这个链表是否已经转为红黑树
else if (e instanceof HashMap.TreeNode)
// 在split函数中可能由于红黑树的长度小于等于UNTREEIFY_THRESHOLD(6)
// 则把红黑树重新转为链表
((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 运行到这里证明桶中有多个节点。
HashMap.Node<K,V> loHead = null, loTail = null;
HashMap.Node<K,V> hiHead = null, hiTail = null;
HashMap.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;
}
- 在达到最大值MAXIMUM_CAPACITY之后仍可以put数据。
- 带构造参数初始化过程中,实际创建的容量并不等于设置的初始容量。tableSizeFor()方法可以自动的将传入的容量转换2的n次方。
- 红黑树可以退化成链表。
- 需要注意的是,扩容操作需要把oldTable的所有键值对重新插入newTable中,因此,这一步是很耗时的。
Java HashMap底层实现原理源码分析Jdk8的更多相关文章
- Java ArrayList底层实现原理源码详细分析Jdk8
简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用 ...
- Spring Boot自动装配原理源码分析
1.环境准备 使用IDEA Spring Initializr快速创建一个Spring Boot项目 添加一个Controller类 @RestController public class Hell ...
- java 1.8 动态代理源码分析
JDK8动态代理源码分析 动态代理的基本使用就不详细介绍了: 例子: class proxyed implements pro{ @Override public void text() { Syst ...
- Java中HashMap底层原理源码分析
在介绍HashMap的同时,我会把它和HashTable以及ConcurrentHashMap的区别也说一下,不过本文主要是介绍HashMap,其实它们的原理差不多,都是数组加链表的形式存储数据,另外 ...
- SpringMvc 启动原理源码分析
了解一个项目启动如何实现是了解一个框架底层实现的一个必不可少的环节.从使用步骤来看,我们一般是引入包之后,配置web.xml文件.官方文档示例的配置如下: <web-app> <se ...
- jQuery1.9.1--结构及$方法的工作原理源码分析
jQuery的$方法使用起来非常的多样式,接口实在太灵活了,有点违反设计模式的原则职责单一.但是用户却非常喜欢这种方式,因为不用记那么多名称,我只要记住一个$就可以实现许多功能,这个$简直就像个万能的 ...
- HashMap实现原理及源码分析之JDK8
继续上回HashMap的学习 HashMap实现原理及源码分析之JDK7 转载 Java8源码-HashMap 基于JDK8的HashMap源码解析 [jdk1.8]HashMap源码分析 一.H ...
- java-通过 HashMap、HashSet 的源码分析其 Hash 存储机制
通过 HashMap.HashSet 的源码分析其 Hash 存储机制 集合和引用 就像引用类型的数组一样,当我们把 Java 对象放入数组之时,并非真正的把 Java 对象放入数组中.仅仅是把对象的 ...
- 【转】HashMap实现原理及源码分析
哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景极其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出 ...
随机推荐
- js运动基础2(运动的封装)
简单运动的封装 先从最简单的封装开始,慢慢的使其更丰富,更实用. 还是上一篇博文的代码,在此不作细说. 需求:点击按钮,让元素匀速运动. <!DOCTYPE html> <html ...
- mysql初识笔记
一.初始mysql mysql介绍: mysql版本: 版本号=3个数字+1个后缀 mysql-5.0.9-beta 5 0 9 Beta 主版本号 发行级别 发行稳定级别 发行系列 发行系列的版本号 ...
- 利用threading模块开线程
一多线程的概念介绍 threading模块介绍 threading模块和multiprocessing模块在使用层面,有很大的相似性. 二.开启多线程的两种方式 1.创建线程的开销比创建进程的开销小, ...
- vmware上安装centos7虚拟机
1.1 Linux 的安装 安 装 采 用 在 虚 拟 机 中 安 装 , 以 方 便 不 同 班 级 授 课 时 , 需 要 重 复 安装的情况. 1.1.1 配置虚拟机 1. 在 VMware W ...
- Nginx负载均衡配置实例
面对高并发的问题,企业往往会从两个方面来解决.其一,从硬件上面,提升硬件的配置,增加服务器的性能:另外,就是从软件上,将数据库和WEB服务器分离,使数据库和WEB服务器都能够充分发挥各自的性能,并且二 ...
- Redis分布式锁的一点小理解
1.在分布式系统中,我们使用锁机制只能保证同一个JVM中一次只有一个线程访问,但是在分布式的系统中锁就不起作用了,这时候就要用到分布式锁(有多种,这里指 redis) 2.在 redis当中可以使用命 ...
- 《完美解决系列》Android5.0以上 Implicit intents with startService are not safe
在Android6.0上,使用了以下代码: Intent intent = new Intent(); intent.setAction("xxx.server"); bindSe ...
- .netCore+Vue 搭建的简捷开发框架 (4)--NetCore 基础 -2
上节中,我们初步的介绍了一下NetCore的一些基础知识,为了控制篇幅(其实也是因为偷懒),我将NetCore 基础分为两部分来写. 0.WebAPI 项目的建立 1..NetCore 项目执行(加载 ...
- 多线程EventWaitHandle -戈多编程
在.NET的System.Threading命名空间中有一个名叫WaitHandler的类,这是一个抽象类(abstract),我们无法手动去创建它,但是WaitHandler有三个子类,这三个子类分 ...
- 跑的比谁都快 51Nod - 1789
香港记者跑的比谁都快是众所周知的常识. 现在,香港记者站在一颗有 nn 个点的树的根结点上(即1号点),编号为 ii 的点拥有权值 a[i]a[i] ,数据保证每个点的编号都小于它任意孩子结 ...