JDK1.8 中的HashMap
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的更多相关文章
- JDK1.7中HashMap死环问题及JDK1.8中对HashMap的优化源码详解
一.JDK1.7中HashMap扩容死锁问题 我们首先来看一下JDK1.7中put方法的源码 我们打开addEntry方法如下,它会判断数组当前容量是否已经超过的阈值,例如假设当前的数组容量是16,加 ...
- JDK1.8中的HashMap实现
1.HashMap概述 在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里.但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通 ...
- JDK1.7 中的HashMap源码分析
一.源码地址: 源码地址:http://docs.oracle.com/javase/7/docs/api/ 二.数据结构 JDK1.7中采用数组+链表的形式,HashMap是一个Entry<K ...
- Jdk1.8中的HashMap实现原理
HashMap概述 HashMap是基于哈希表的Map接口的非同步实现.此实现提供所有可选的映射操作,并允许使用null值和null键.此类不保证映射的顺序,特别是它不保证该顺序恒久不变. HashM ...
- 【1】Jdk1.8中的HashMap实现原理
HashMap概述 HashMap是基于哈希表的Map接口的非同步实现.此实现提供所有可选的映射操作,并允许使用null值和null键.此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 内部实现 ...
- JDK1.8中对hashmap的优化
在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外.HashMap实际上是一个“链表散列”的数据结 ...
- JDK1.8中HashMap实现
JDK1.8中的HashMap实现跟JDK1.7中的实现有很大差别.下面分析JDK1.8中的实现,主要看put和get方法. 构造方法的时候并没有初始化,而是在第一次put的时候初始化 putVal方 ...
- hashMap在jdk1.7与jdk1.8中的原理及不同
在分析jdk1.7中HashMap的hash冲突时,不知大家是否有个疑问就是万一发生碰撞的节点非常多怎么版?如果说成百上千个节点在hash时发生碰撞,存储一个链表中,那么如果要查找其中一个节点,那就不 ...
- java并发:jdk1.8中ConcurrentHashMap源码浅析
ConcurrentHashMap是线程安全的.可以在多线程中对ConcurrentHashMap进行操作. 在jdk1.7中,使用的是锁分段技术Segment.数据结构是数组+链表. 对比jdk1. ...
随机推荐
- java基础-泛型举例详解
泛型 泛型是JDK5.0增加的新特性,泛型的本质是参数化类型,即所操作的数据类型被指定为一个参数.这种类型参数可以在类.接口.和方法的创建中,分别被称为泛型类.泛型接口.泛型方法. 一.认识泛型 在没 ...
- Spring之跨重定向请求传递数据
摘要 在开发场景中,大部分数据都是使用请求转发(forward)进行传递,而使用重定向(redirect)传递数据可能比较少. 那么问题来了:请求中的数据生命周期存活时间只在一个请求转发(reques ...
- CentOS7.2下PXE+kickstart自动化安装系统
一.实验环境 操作系统:CentOS Linux release 7.2.1511 (Core) 网卡地址:192.168.100.147/24 光盘镜像:CentOS-7-x86_64-Minima ...
- python爬虫学习---爬取微软必应翻译(中英互译)
前言本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理.作者:OSinooO 本人属于python新手,刚学习的 python爬虫基础 ...
- 深入理解 Java 方法
方法(有的人喜欢叫函数)是一段可重用的代码段.
- 【算法】273-每周一练 之 数据结构与算法(Tree)
这是第六周的练习题,最近加班比较多. 下面是之前分享的链接: [算法]200-每周一练 之 数据结构与算法(Stack) [算法]213-每周一练 之 数据结构与算法(LinkedList) [算法] ...
- JMeter之SteppingShape
1.背景 其实是这样的,最近包括以前都有同事问过宝路一个问题:JMeter测试计划中涉及到梯度压测时,整个测试计划执行完毕,聚合报告看的是整体的结果啊!并不能直观看到每个梯度下的吞吐量的值(虽然可以通 ...
- Dubbo源码分析之SPI(三)
一.概述 本篇介绍自适应扩展,方法getAdaptiveExtension()的实现.ExtensionLoader类本身很多功能也使用到了自适应扩展.包括ExtensionFactory扩展. 通俗 ...
- OS OSTEP (Operating Systems Three Easy pieces 操作系统导论 )
读<OSTEP>的一点重点记录与感悟 (未完) Chapter-2 第二章 1. 操作系统的设计目标: 抽象.高性能.保护.不间断运行. 抽象:建立一些“抽象”,让操作系统方便和易于使用 ...
- android 7.0+、8.0+应用中点击拍照崩溃的解决办法
在开发中,项目里面明明已经添加过拍照或者读取相册的权限,但是在点击拍照或者打开相册的时候应用会崩溃,报一下错误: Caused by: android.os.FileUriExposedExcepti ...