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、静态变量(默认值)

  1. DEFAULT_INITIAL_CAPACITY= 1 << 4:初始化数组默认长度。1左移4位,为16。
  2. MAXIMUM_CAPACITY = 1 << 30:初始化默认容量大小,2的30次方。
  3. DEFAULT_LOAD_FACTOR = 0.75f:负载因子,用于和数组长度相乘,当数组长度大于得到的值后,会进行数组的扩容,扩容倍数是2^n。
  4. TREEIFY_THRESHOLD = 8:链表长度达到该值后,会进行数据结构转换,变成红黑树,优化速率。
  5. 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、主要方法

  1. 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链表中。其余具体细节,在上面源码中已经标注。

  2. 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的更多相关文章

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

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

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

    一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也 ...

  3. 【集合框架】JDK1.8源码分析之HashMap & LinkedHashMap迭代器(三)

    一.前言 在遍历HashMap与LinkedHashMap时,我们通常都会使用到迭代器,而HashMap的迭代器与LinkedHashMap迭代器是如何工作的呢?下面我们来一起分析分析. 二.迭代器继 ...

  4. JDK1.8源码分析之HashMap(一) (转)

    一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也 ...

  5. 【源码分析】HashMap源码再读-基于Java8

    最近工作不是太忙,准备再读读一些源码,想来想去,还是先从JDK的源码读起吧,毕竟很久不去读了,很多东西都生疏了.当然,还是先从炙手可热的HashMap,每次读都会有一些收获.当然,JDK8对HashM ...

  6. JDK1.8源码分析之HashMap

    一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也 ...

  7. Java高并发程序设计学习笔记(五):JDK并发包(各种同步控制工具的使用、并发容器及典型源码分析(Hashmap等))

    转自:https://blog.csdn.net/dataiyangu/article/details/86491786#2__696 1. 各种同步控制工具的使用1.1. ReentrantLock ...

  8. 【集合框架】JDK1.8源码分析之HashMap

    一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也 ...

  9. java读源码 之 map源码分析(HashMap,图解)一

    ​ 开篇之前,先说几句题外话,写博客也一年多了,一直没找到一种好的输出方式,博客质量其实也不高,很多时候都是赶着写出来的,最近也思考了很多,以后的博客也会更注重质量,同时也尽量写的不那么生硬,能让大家 ...

随机推荐

  1. PostgreSQL 表字段起别名

    使用Postgreq Sql 表字段起别名时注意要用双引号,使用单引号会出现语法错误,执行结果如图

  2. 快速掌握MIPI开发攻略,对接百度人工智能计算卡EdgeBoard

    MIPI(移动行业处理器接口)是Mobile Industry Processor Interface的缩写,是MIPI联盟发起的为移动应⽤处理器制定的开放标准.MIPI采⽤高速串行接口传输数据,满⾜ ...

  3. cgo在mac上编译

    用了cgo mac上编译不过的可以试试下面的方法 ../../pkg/mod/github.com/mattn/go-sqlite3@v1.:: fatal error: 'stdlib.h' fil ...

  4. JavaScript 是如何运行的?

    摘要: 理解JS执行原理. 原文:JavaScript 是如何运行的? 作者:hengg Fundebug经授权转载,版权归原作者所有. 什么是JavaScript? 我们来确认一下JavaScrip ...

  5. RAC_多路径配置

    多路径配置 http://blog.itpub.net/31397003/viewspace-2143390/ 挂盘/配置好yum源 2.程序包的安装 device-mapper-1.02.95-2. ...

  6. bay——巡检RAC操作.txt

    Oracle Cluster Software 包括下列组件:Event Management (EVM)Cluster Synchronization Services (CSS)Cluster R ...

  7. Rman Enhancements(增强) In Oracle 11g. (Doc ID 1115423.1)

    Rman Enhancements In Oracle 11g. (Doc ID 1115423.1) APPLIES TO: Oracle Database - Enterprise Edition ...

  8. 006.MongoDB副本集

    一 MongoDB 复制(副本集) 1.1 复制概述 MongoDB复制是将数据同步在多个服务器的过程. 复制提供了数据的冗余备份,并在多个服务器上存储数据副本,提高了数据的可用性, 并可以保证数据的 ...

  9. Day11 - Python基础11 模块学习——optparse

    Python 有两个内建的模块用于处理命令行参数: 一个是 getopt,<Deep in python>一书中也有提到,只能简单处理 命令行参数: 另一个是 optparse,它功能强大 ...

  10. day95_11_28,selenium定位元素,cookies获取

    一.selenium selenium可以将一些资源定位: # 1.find_element_by_id 根据id找 # 2.find_element_by_link_text 根据链接名字找到控件( ...