​ 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. tp5引用第三方类vendor或extend(多种方法)

    extend 方法一:命名空间引入 我们只需要把自己的类库包目录放入EXTEND_PATH目录(默认为extend,可配置),就可以自动注册对应的命名空间,例如: 我们在extend目录下面新增一个l ...

  2. 一条数据的HBase之旅,简明HBase入门教程2:数据模型

    [摘要] 上一篇文章讲了HBase项目与应用概况信息,这篇文章讲述HBase的数据模型以及一些基础概念,数据模型可以说决定了HBase适合于什么应用场景. 华为云上的NoSQL数据库服务CloudTa ...

  3. 成功build Maven但eclipse中依然显示该工程有错误

    在mac pro的控制台中,成功执行 mvn package,但该工程在eclipse仍然显示有错误. 解决办法:右键该工程 -> Maven -> Update project,勾选 F ...

  4. Unity3d 与 Objective-C 数据交互,持续更新中

    Unity 3D是用于3D游戏编程的语言,它是一个用C\C++编写的强大的库.而在游戏中经常要接入用OC编写的SDK,这就涉及到了Unity3d 和 OC之间的数据交互.XCode是完成兼容C语言的. ...

  5. linux gre隧道创建

    目录 linux gre隧道创建 实验环境 实验目的 实验步骤 1.在host A(10.10.10.47)上面操作 2.在host B(192.168.0.118)上面操作 实验结果 还原实验环境 ...

  6. 【广州.NET社区推荐】【译】Visual Studio 2019 中 WPF & UWP 的 XAML 开发工具新特性

    原文 | Dmitry 翻译 | 郑子铭 自Visual Studio 2019推出以来,我们为使用WPF或UWP桌面应用程序的XAML开发人员发布了许多新功能.在本周的 Visual Studio ...

  7. Redis实战 | 5种Redis数据类型详解

    我们知道Redis是目前非常主流的KV数据库,它因高性能的读写能力而著称,其实还有另外一个优势,就是Redis提供了更加丰富的数据类型,这使得Redis有着更加广泛的使用场景.那Redis提供给用户的 ...

  8. 成为java高手的成长历程想学好java必看

    1:J2SE入门阶段(3-6个月) 学习内容:J2SE常用类,JDBC等等 动态绑定,反射机制等 2:J2EE入门阶段(3个月) 学习内容:JSP/Servlet/JavaBean MVC结构的概念 ...

  9. leetcode-算法系列-两数之和

    本系列的题目都是出自于"leetcode" 用博客记录是为了加强自己的记忆与理解,也希望能和大家交流更好更优的解题思路. 题目: 给定一个整数数组和一个目标值,找出数组中和为目标值 ...

  10. 建议2:注意Javascript数据类型的特殊性---(3)正确检测数据类型

    使用typeof预算符返回一个用于识别其运算数类型的字符串.对于任何变量来说,使用typeof预算符总是以字符串的形式返回一下6种类型之一 number string boolean object f ...