​ HashMap本质上Java中的一种数据结构,他是由数组+链表的形式组织而成的,当然了在jdk1.8后,当链表长度大于8的时候为了快速寻址,将链表修改成了红黑树。

​ 既然本质上是一个数组,那我们应该把对应的键值对放到数组的哪个位置就成了重中之重,因为要保证这个算法对同一个key在同一个数组中每次计算出来的结果的一致性进行保证(幂等性)。那有的同学就要说了,很简单啊,我们获取key的hashCode然后将结果和数组长度进行取模就可以了。

​ 这个办法的确可行,但是jdk中是这样做的吗?为什么?这就要对源码进行分析了。

​ 首先,只要用过hashMap的同学肯定知道,hashMap是put操作的时候才会将键值对放在数组上,那我们看一下HashMap的put源码吧。

	/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 可以看到put操作执行了putValue这个方法,参数有五个,但是第一个参数是获取的hash(Object key) 方法的返回结果,那我们一起来看一下这个方法

说一说JDK1.8中HashMap中的hash算法

​ hash算法源码如下:

	static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

​ 方法非常简单,首先声明了一个int类型的局部变量h;然后判断key是否为null,如果是null的话直接返回0,否则进行计算,计算公式为

获取key的hashCode值并赋值给局部变量h,然后将h与h右移16的结果进行异或运算

​ 简单来说就是这样的,假设之前的hashcode值为:

​ 1111 1111 1111 1111 1001 1101 1100 0011

​ 右移16位为:

​ 0000 0000 0000 0000 1111 1111 1111 1111

​ 进行异或运算(这里简单科普一下异或运算的计算公式,如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。)后的结果为:

​ 1111 1111 1111 1111 1001 1101 1100 0011

​ 0000 0000 0000 0000 1111 1111 1111 1111

​ 1111 1111 1111 1111 0110 0010 0011 1100

​ 相信这个方法大家都能看得懂,但是看懂归看懂,可是我不明白为什么要这么做。这里先卖个关子,我们带着疑问继续往下看,put的实现。

JDK1.8以后Hash'Map中的寻址算法

我们回到hashMap中的put方法

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

可以看到其调用了putVal方法 这段代码有点长,我们一点一点分析

	/**
* Implements Map.put and related methods.
* 实现Map.put和相关的方法
* @param hash key的hash值(这里是前面hash()方法算出来的值)
* @param key put的key
* @param value put的value
* @param onlyIfAbsent 如果为true的话,不改变现有的值
* @param evict 如果为false,则表处于创建模式。
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 声明节点类数组tab 节点p 数组长度n 键值对存放位置i
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果没有初始化table,则初始化,长度默认设置为16 --> n
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 这里就声明了我们put的键值对应该存放在数组的什么位置
// p = tab[i = (n - 1) & hash]) i = 数组位置
// 如果这个数组下标对应的是null,就直接新建node写入到这个位置上,否则就得组织链表或红黑树了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
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) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

从源码可发现hashMap在put的时候,寻址算法的公式是:tab[i = (n - 1) & hash ])

这里就出现了疑问,为什么是这么些,用hashCode直接取模不香吗?这里先抛出一个公式如果n为2的次方数,那么 hascode%n = (n - 1) & hash,我们可以测试一下,如果大家也感兴趣,可以自己试试:

    public static void main(String[] args) {

        String[] strings = new String[16];
String key = "qq1";
int hash = key.hashCode();
int length = strings.length;
System.out.println(hash % length);
System.out.println((length - 1) & hash); }
// out true public static void main(String[] args) { String[] strings = new String[64];
String key = "zxl1";
int hash = key.hashCode();
int length = strings.length;
System.out.println((hash % length) == ((length - 1) & hash));
}
// out true

那又出现了一个问题,为什么不用hashCode对数组长度取模的方式呢?我们来做个简单的测试

	public static void main(String[] args) {

        String[] strings = new String[64];
String key = "zxl1";
int hash = key.hashCode();
int length = strings.length;
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
int k = hash % length;
}
System.out.println("----取模算法耗时 : " + (System.currentTimeMillis() - startTime) + "ms ----");
startTime = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
int k = (length - 1) & hash;
}
System.out.println("----与运算耗时 : " + (System.currentTimeMillis() - startTime) + "ms ----");
} /**
* output
* ----取模算法耗时 : 2ms ----
* ----与运算耗时 : 1ms ----
*/

相对取模运算来说,使用与运算效率更高,所以jdk的hashmap的寻址算法采用了与算法来参与运算。

看到这里很多同学就要说了,这里只是说了为什么采用与运算,可是没有说为什么hashMap中的hash是自己运算的一个结果,而不是直接使用hashcode啊。其实这和(n - 1) & hash有关。我们还是拿上面举的例子来说:

	hashCode
1111 1111 1111 1111 1001 1101 1100 0011
hashCode右移16位
0000 0000 0000 0000 1111 1111 1111 1111
异或运算后的结果
1111 1111 1111 1111 0110 0010 0011 1100

如果我们直接用hashcode与n-1(假设使用默认长度16)进行与运算(两位同时为“1”,结果才为“1”,否则为0)的话,结果是这样的

1111 1111 1111 1111 1001 1101 1100 0011
0000 0000 0000 0000 0000 0000 0000 1111
0000 0000 0000 0000 0000 0000 0000 0011

ok,我们可以获取到这个key在数组中的位置应该是3,那么问题来了,假设我们还有一个key的hashcode后16位和我们现在这个key的hashcode完全一致,但是前16位略有不同例如

1111 1111 1001 1010 1001 1101 1100 0011
0000 0000 0000 0000 0000 0000 0000 1111
0000 0000 0000 0000 0000 0000 0000 0011

我们发现两个完全不一致的hashcode算出来的结果是一样的,都是3。原因是因为在与运算的时候,前面的16位根本没有发挥作用,所以会导致大量的结果是一致的。

这样就可以解释为什么hash方法里面要将hashcode的值右移16位再进行异或预算了,这样的目的是为了让前16位和后16位都参与运算。

还是刚才的例子,我们来看一下区别:

1111 1111 1111 1111 1001 1101 1100 0011				hashcode
0000 0000 0000 0000 1111 1111 1111 1111 hashcode右移16位
1111 1111 1111 1111 0110 0010 0011 1100 hash算法结果
0000 0000 0000 0000 0000 0000 0000 1111 n-1
0000 0000 0000 0000 0000 0000 0000 1100 与预算结果=12 ----------------------------------------------------------------------------------
1111 1111 1001 1010 1001 1101 1100 0011 hashcode
0000 0000 0000 0000 1111 1111 1001 1010 hashcode右移16位
1111 1111 1001 1010 0110 0010 0111 1101 hash算法结果
0000 0000 0000 0000 0000 0000 0000 1111 n-1
0000 0000 0000 0000 0000 0000 0000 1101 与预算结果=13

这样就能尽可能地保证不同的hashcode能均匀的分配在数组上。


未完待续

JDK1.8 中的HashMap的更多相关文章

  1. JDK1.7中HashMap死环问题及JDK1.8中对HashMap的优化源码详解

    一.JDK1.7中HashMap扩容死锁问题 我们首先来看一下JDK1.7中put方法的源码 我们打开addEntry方法如下,它会判断数组当前容量是否已经超过的阈值,例如假设当前的数组容量是16,加 ...

  2. JDK1.8中的HashMap实现

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

  3. JDK1.7 中的HashMap源码分析

    一.源码地址: 源码地址:http://docs.oracle.com/javase/7/docs/api/ 二.数据结构 JDK1.7中采用数组+链表的形式,HashMap是一个Entry<K ...

  4. Jdk1.8中的HashMap实现原理

    HashMap概述 HashMap是基于哈希表的Map接口的非同步实现.此实现提供所有可选的映射操作,并允许使用null值和null键.此类不保证映射的顺序,特别是它不保证该顺序恒久不变. HashM ...

  5. 【1】Jdk1.8中的HashMap实现原理

    HashMap概述 HashMap是基于哈希表的Map接口的非同步实现.此实现提供所有可选的映射操作,并允许使用null值和null键.此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 内部实现 ...

  6. JDK1.8中对hashmap的优化

    在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外.HashMap实际上是一个“链表散列”的数据结 ...

  7. JDK1.8中HashMap实现

    JDK1.8中的HashMap实现跟JDK1.7中的实现有很大差别.下面分析JDK1.8中的实现,主要看put和get方法. 构造方法的时候并没有初始化,而是在第一次put的时候初始化 putVal方 ...

  8. hashMap在jdk1.7与jdk1.8中的原理及不同

    在分析jdk1.7中HashMap的hash冲突时,不知大家是否有个疑问就是万一发生碰撞的节点非常多怎么版?如果说成百上千个节点在hash时发生碰撞,存储一个链表中,那么如果要查找其中一个节点,那就不 ...

  9. java并发:jdk1.8中ConcurrentHashMap源码浅析

    ConcurrentHashMap是线程安全的.可以在多线程中对ConcurrentHashMap进行操作. 在jdk1.7中,使用的是锁分段技术Segment.数据结构是数组+链表. 对比jdk1. ...

随机推荐

  1. Vue组件应用

    Vue的组件是可复用的 Vue 实例,且带有一个名字 .我们可以在一个通过 new Vue 创建的 Vue 根实例中,把这个组件作为自定义元素来使用.因为组件是可复用的 Vue 实例,所以它们与 ne ...

  2. 华为云MySQL金融版正式商用,高可靠的金融级数据库来了

    摘要:全新三节点架构,基于深度优化的MGR组复制技术,提供金融级的数据一致性. 日前,华为云数据库MySQL 金融版正式商业化发布,MySQL金融版采用MGR技术,基于Paxos协议,采用一主两备三节 ...

  3. 华为ARM64服务器上手体验--不吹不黑,用实际应用来看看TaiShan鲲鹏的表现

    背景 中美贸易冲突以来,相信最大的感受,并不是我对你加多少关税,而是我有,可我不卖给你."禁售"成了市场经济中最大的竞争力. 相信也是因为这个原因,华为"备胎转正&quo ...

  4. [转载]C++中四种强制类型转换方式

    C++中四种强制类型转换方式 原文地址:http://www.cnblogs.com/home123/p/6763967.html 类型转换有c风格的,当然还有c++风格的.c风格的转换的格式很简单( ...

  5. luogu P1582 倒水 |数学

    题目描述 一天,CC买了N个容量可以认为是无限大的瓶子,开始时每个瓶子里有1升水.接着~~CC发现瓶子实在太多了,于是他决定保留不超过K个瓶子.每次他选择两个当前含水量相同的瓶子,把一个瓶子的水全部倒 ...

  6. [TimLinux] JavaScript 模态框可拖动功能实现——节流版

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  7. 牛客练习赛32 B题 Xor Path

    链接:https://ac.nowcoder.com/acm/contest/272/B来源:牛客网 题目描述 给定一棵n个点的树,每个点有权值.定义表示  到  的最短路径上,所有点的点权异或和. ...

  8. JPEG算法解密

    图片压缩有多重要,可能很多人可能并没有一个直观上的认识,举个例子,一张800X800大小的普通图片,如果未经压缩,大概在1.7MB左右,这个体积如果存放文本文件的话足够保存一部92万字的鸿篇巨著< ...

  9. 为什么有ASP.NET

    最近读了一些文章,总结一下: 在1999年,当时微软的windows系统运行的所有的应用程序都是有组件对象模型为根本基础开发的,用VB来处理数据访问和复杂的用户界面,缺点是不能使用函数指针,因为当时的 ...

  10. H5 app在真机调试的时候正常,打包成app后报错

    在自己的一个用h5开发的项目中, 环境 IDE HBuilderX 打包工具 Hbuilder线上打包 开发语言 JS 现象 从一个列表进入详细页之后一直转圈圈.因为是调用系统原生的等待组件,界面无法 ...