在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()流程:

    1. 判断键值对数组table[]是否为空或为null,否则以默认大小resize();
    2. 根据键key计算hash值与table的长度进行与运算得到插入的数组索引 index,如果tab[index] == null,直接根据key-value新建node添加,否则转入3
    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值,

    1. 计算hash & (table.len - 1)得到在链表数组中的位置,
    2. 先判断node链表(桶)中的第一个节点的key是否与参数key相等,
    3. 不等则判断是否已经转为红黑树,若转为红黑树则在红黑树中查找,
    4. 如没有转为红黑树就遍历后面的链表找到相同的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的更多相关文章

  1. Java ArrayList底层实现原理源码详细分析Jdk8

    简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用 ...

  2. Spring Boot自动装配原理源码分析

    1.环境准备 使用IDEA Spring Initializr快速创建一个Spring Boot项目 添加一个Controller类 @RestController public class Hell ...

  3. java 1.8 动态代理源码分析

    JDK8动态代理源码分析 动态代理的基本使用就不详细介绍了: 例子: class proxyed implements pro{ @Override public void text() { Syst ...

  4. Java中HashMap底层原理源码分析

    在介绍HashMap的同时,我会把它和HashTable以及ConcurrentHashMap的区别也说一下,不过本文主要是介绍HashMap,其实它们的原理差不多,都是数组加链表的形式存储数据,另外 ...

  5. SpringMvc 启动原理源码分析

    了解一个项目启动如何实现是了解一个框架底层实现的一个必不可少的环节.从使用步骤来看,我们一般是引入包之后,配置web.xml文件.官方文档示例的配置如下: <web-app> <se ...

  6. jQuery1.9.1--结构及$方法的工作原理源码分析

    jQuery的$方法使用起来非常的多样式,接口实在太灵活了,有点违反设计模式的原则职责单一.但是用户却非常喜欢这种方式,因为不用记那么多名称,我只要记住一个$就可以实现许多功能,这个$简直就像个万能的 ...

  7. HashMap实现原理及源码分析之JDK8

    继续上回HashMap的学习 HashMap实现原理及源码分析之JDK7 转载 Java8源码-HashMap  基于JDK8的HashMap源码解析  [jdk1.8]HashMap源码分析 一.H ...

  8. java-通过 HashMap、HashSet 的源码分析其 Hash 存储机制

    通过 HashMap.HashSet 的源码分析其 Hash 存储机制 集合和引用 就像引用类型的数组一样,当我们把 Java 对象放入数组之时,并非真正的把 Java 对象放入数组中.仅仅是把对象的 ...

  9. 【转】HashMap实现原理及源码分析

    哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景极其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出 ...

随机推荐

  1. python压测工具Locust

    python压测工具Locust Locust介绍 Locust作为基于Python语言的性能测试框架. 其优点在于他的并发量可以实现单机10倍于LoadRunner和Jmeter工具.他的工作原理为 ...

  2. Java源码跟踪阅读技巧

    转:https://www.jianshu.com/p/ab865109070c 本文基于Eclipse IDE 1.Quick Type Hierarchy 快速查看类继承体系. 快捷键:Ctrl ...

  3. Git项目分支分配

    主要分支包含master分支与develop分支,临时分支可以分为: release: 从develop分出 ,是最终要发布的版本. feature: 实现某功能时推荐新建分支,从develop分出. ...

  4. go语言标准库之http/template

    html/template包实现了数据驱动的模板,用于生成可对抗代码注入的安全HTML输出.它提供了和text/template包相同的接口,Go语言中输出HTML的场景都应使用text/templa ...

  5. 尝鲜 vue3.x 新特性 - CompositionAPI

    0. 基础要求 了解常见的 ES6 新特性 ES6 的导入导出语法 解构赋值 箭头函数 etc... 了解 vue 2.x 的基本使用 组件 常用的指令 生命周期函数 computed.watch.r ...

  6. ADB命令无法导出文件到物理机上处理办法

    因为想查看一下脚本生成的sqlite文件.就想导出文件,,结果导出adb pull命令一直报错.使用su也是错误的..最后发现adb pull 不能再adb的命令状态下执行.需要退出adb命令.然后直 ...

  7. 使用golang插入mysql性能提升经验

    前言 golang可以轻易制造高并发,在某些场景很合适,比如爬虫的时候可以爬的更加高效.但是对应某些场景,如文件读写,数据库访问等IO为瓶颈的场合,就没有什么优势了. 前提基础 1.golang数据库 ...

  8. Java9以后的垃圾回收

    1: finalize() 方法 finallize() 方法是Object类的方法, 用于在类被GC回收时 做一些处理操作, 但是JVM并不能保证finalize(0 ) 方法一定被执行, 由于fi ...

  9. 新建springmvc配置文件

    新建spring或springmvc的配置文件时,需要先加入spring-bean-4.3.18.RELEASE.jar包,当然可以是其他版本,这样就可以在资源目录下,比如resources(Reso ...

  10. centos 7安装rac 11gR2时运行root.sh报错找不到ohas服务(ohasd failed to start)

    单独在linux 7中为ohasd设置一个服务.步骤如下1. 创建服务ohas.service的服务文件并赋予权限touch /usr/lib/systemd/system/ohas.servicec ...