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. ...
随机推荐
- Spring boot如何快速的配置多个Redis数据源
简介 redis 多数据源主要的运用场景是在需要使用多个redis服务器或者使用多个redis库,本文采用的是fastdep依赖集成框架,快速集成Redis多数据源并集成lettuce连接池,只需引入 ...
- 人人学IoT 助学思维导图
原来学IoT记录的学习笔记,学完之后,对考试和工作都有些帮助,特分享给大家 笔记分享链接 https://share.mindmanager.com/#publish/s6TqusKeSG6aflXL ...
- 区块链学习笔记:D03 区块链在各行业领域的应用(一)
今天主要是学习了区块链在金融和供应链领域的应用,重点体现了区块链多方参与.透明可信.防篡改防抵赖的技术优势 区块链的应用场景最早是在金融行业应用较多,后续逐步扩展到传统行业,如:供应链.政务服务.物联 ...
- 【开发者portal在线开发插件系列三】字符串 及 可变长度字符串
基础篇 基础场景见上面两个帖子,这里单独说明字符串和可变长度字符串的用法. 话不多说,开始今天的演(表)示(演) Profile和插件开发 添加一个string类型的属性: 在插件里添加一条数据上报消 ...
- swift实现单例的四种方式
单例模式 单例模式是设计模式中最简单的一种,甚至有些模式大师都不称其为模式,称其为一种实现技巧,因为设计模式讲究对象之间的关系的抽象,而单例模式只有自己一个对象. 当你只需要一个实例的时候需要使用单例 ...
- react-native-linear-gradient颜色渐变
目录 一 安装 二 使用 2.1 colors 2.2 start / end eg1:斜角渐变 eg2: 从左到右 2.2 locations eg1: 0.4是渐变的起点,0.6是渐变的终点 一 ...
- ACM小组的古怪象棋
Description ACM小组的Samsara和Staginner对中国象棋特别感兴趣,尤其对马(可能是因为这个棋子的走法比较多吧)的使用进行深入研究.今天他们又在 构思一个古怪的棋局:假如Sam ...
- 记录一些实用的小技巧-CSS篇
1.单行文本截断 .text{ width: 200px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } 2.多行 ...
- Everspin非易失性MRAM切换技术
切换MRAM技术 切换MRAM使用1个晶体管,1个MTJ单元来提供简单的高密度存储器.Everspin使用获得专利的Toggle电池设计,可提供高可靠性.数据在温度下20年始终是非易失性的. 在读取期 ...
- 【算法】331- JS洗牌算法
点击上方"前端自习课"关注,学习起来~ 最近的一个塔罗牌项目中,有一个洗牌的需求,其实也就是随机打乱数组,遂网上搜了下,再此做个整理- 塔罗牌 举例来说,我们有一个如下图所示的数组 ...