HashMap JDK 1.8 VS JDK1.7 增加性能
一、传统 HashMap的缺点
(1)JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。
(2)当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。
(3)针对这种情况,JDK 1.8 中引入了红黑树(查找时间复杂度为 O(logn))来优化这个问题
二、JDK1.8中HashMap的数据结构
2.1HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
}
|
2.2HashMap 中关于红黑树的三个关键参数
TREEIFY_THRESHOLD
|
UNTREEIFY_THRESHOLD
|
MIN_TREEIFY_CAPACITY
|
---|---|---|
|
static final int UNTREEIFY_THRESHOLD = 6 |
|
|
|
|
2.3HashMap 在 JDK 1.8 中新增的操作:桶的树形化 treeifyBin()
在Java 8 中,如果一个桶中的元素个数超过 TREEIFY_THRESHOLD(默认是 8 ),就使用红黑树来替换链表,从而提高速度。
这个替换的方法叫 treeifyBin() 即树形化。
//将桶内所有的 链表节点 替换成 红黑树节点
1 final void treeifyBin(Node<K,V>[] tab, int hash) {
2 int n, index; Node<K,V> e;
3 //如果当前哈希表为空,或者哈希表中元素的个数小于 进行树形化的阈值(默认为 64),就去新建/扩容
4 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
5 resize();
6 else if ((e = tab[index = (n - 1 ) & hash]) != null ) {
7 //如果哈希表中的元素个数超过了 树形化阈值,进行树形化
8 // e 是哈希表中指定位置桶里的链表节点,从第一个开始
9 TreeNode<K,V> hd = null , tl = null ; //红黑树的头、尾节点
10 do {
11 //新建一个树形节点,内容和当前链表节点 e 一致
12 TreeNode<K,V> p = replacementTreeNode(e, null );
13 if (tl == null ) //确定树头节点
14 hd = p;
15 else {
16 p.prev = tl;
17 tl.next = p;
18 }
19 tl = p;
20 } while ((e = e.next) != null );
21 //让桶的第一个元素指向新建的红黑树头结点,以后这个桶里的元素就是红黑树而不是链表了
22 if ((tab[index] = hd) != null )
23 hd.treeify(tab);
24 }
25 }
26 TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
27 return new TreeNode<>(p.hash, p.key, p.value, next);
28 }
|
上述操作做了这些事:
(1)根据哈希表中元素个数确定是扩容还是树形化
(2)如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系
(3)然后让桶第一个元素指向新建的树头结点,替换桶的链表内容为树形内容
三、分析HashMap的put方法
3.1HashMap的put方法执行过程可以通过下图来理解,自己有兴趣可以去对比源码更清楚地研究学习。
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
1 public V put(K key, V value) {
2 // 对key的hashCode()做hash
3 return putVal(hash(key), key, value, false , true );
4 }
5
6 final V putVal( int hash, K key, V value, boolean onlyIfAbsent,
7 boolean evict) {
8 Node<K,V>[] tab; Node<K,V> p; int n, i;
9 // 步骤①:tab为空则创建
10 if ((tab = table) == null || (n = tab.length) == 0 )
11 n = (tab = resize()).length;
12 // 步骤②:计算index,并对null做处理
13 if ((p = tab[i = (n - 1 ) & hash]) == null )
14 tab[i] = newNode(hash, key, value, null );
15 else {
16 Node<K,V> e; K k;
17 // 步骤③:节点key存在,直接覆盖value
18 if (p.hash == hash &&
19 ((k = p.key) == key || (key != null && key.equals(k))))
20 e = p;
21 // 步骤④:判断该链为红黑树
22 else if (p instanceof TreeNode)
23 e = ((TreeNode<K,V>)p).putTreeVal( this , tab, hash, key, value);
24 // 步骤⑤:该链为链表
25 else {
26 for ( int binCount = 0 ; ; ++binCount) {
27 if ((e = p.next) == null ) {
28 p.next = newNode(hash, key,value, null );
//链表长度大于8转换为红黑树进行处理
29 if (binCount >= TREEIFY_THRESHOLD - 1 ) // -1 for 1st
30 treeifyBin(tab, hash);
31 break ;
32 }
// key已经存在直接覆盖value
33 if (e.hash == hash &&
34 ((k = e.key) == key || (key != null && key.equals(k))))
35 break ;
36 p = e;
37 }
38 }
39
40 if (e != null ) { // existing mapping for key
41 V oldValue = e.value;
42 if (!onlyIfAbsent || oldValue == null )
43 e.value = value;
44 afterNodeAccess(e);
45 return oldValue;
46 }
47 }
48 ++modCount;
49 // 步骤⑥:超过最大容量 就扩容
50 if (++size > threshold)
51 resize();
52 afterNodeInsertion(evict);
53 return null ;
54 }
|
1 public V put(K key, V value) {
2 if (table == EMPTY_TABLE) { //空表table的话,根据size的阈值填充
3 inflateTable(threshold);
4 }
5 if (key == null )
6 return putForNullKey(value);
7 int hash = hash(key); //成hash,得到索引Index的映射
8 int i = indexFor(hash, table.length);
9 for (Entry<K,V> e = table[i]; e != null ; e = e.next) { //遍历当前索引的冲突链,找是否存在对应的key
10 Object k;
11 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //如果存在对应的key, 则替换oldValue并返回oldValue
12 V oldValue = e.value;
13 e.value = value;
14 e.recordAccess( this );
15 return oldValue;
16 }
17 }
18 //冲突链中不存在新写入的Entry的key
19 modCount++;
20 addEntry(hash, key, value, i);
21 return null ;
22 }
|
3.2HashMap 在 JDK 1.8 中新增的操作: 红黑树中查找元素 getTreeNode()
*/
1 final Node<K,V> getNode( int hash, Object key) {
2 Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
3 if ((tab = table) != null && (n = tab.length) > 0 &&
4 (first = tab[(n - 1 ) & hash]) != null ) {
5 if (first.hash == hash && // always check first node
6 ((k = first.key) == key || (key != null && key.equals(k))))
7 return first;
8 if ((e = first.next) != null ) {
9 if (first instanceof TreeNode)
10 return ((TreeNode<K,V>)first).getTreeNode(hash, key);
11 do {
12 if (e.hash == hash &&
13 ((k = e.key) == key || (key != null && key.equals(k))))
14 return e;
15 } while ((e = e.next) != null );
16 }
17 }
18 return null ;
19 }
|
(1)HashMap 的查找方法是 get(),它通过计算指定 key 的哈希值后,调用内部方法 getNode();
2)这个 getNode() 方法就是根据哈希表元素个数与哈希值求模(使用的公式是 (n - 1) &hash
)得到 key 所在的桶的头结点,如果头节点恰好是红黑树节点,
就调用红黑树节点的 getTreeNode() 方法,否则就遍历链表节点。
(3)getTreeNode 方法使通过调用树形节点的 find()方法进行查找:
1 final TreeNode<K,V> getTreeNode( int h, Object k) {
2 return ((parent != null ) ? root() : this ).find(h, k, null );
3 }
|
(4)由于之前添加时已经保证这个树是有序的,因此查找时基本就是折半查找,效率很高。
(5)这里和插入时一样,如果对比节点的哈希值和要查找的哈希值相等,就会判断 key 是否相等,相等就直接返回;不相等就从子树中递归查找。
3.3 JDK1.8 VS JDK1.7 扩容机制
下面举个例子说明下扩容过程。假设了我们的hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)。其中的哈希桶数组table的size=2, 所以key = 3、7、5,put顺序依次为 5、7、3。在mod 2以后都冲突在table[1]这里了。这里假设负载因子 loadFactor=1,即当键值对的实际大小size 大于 table的实际大小时进行扩容。接下来的三个步骤是哈希桶数组 resize成4,然后所有的Node重新rehash的过程。
下面我们讲解下JDK1.8做了哪些优化。经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:
这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。
四,JDK1.7 VS JDK1.8的性能
4.1 put操作
1.hash比较均匀的时候(负载因子时0.75导致的)
次数
|
10
|
100
|
1000
|
10000
|
100000
|
---|---|---|---|---|---|
JDK1.7时间(ns) | 1100 | 720 | 832 | 914 | 912 |
JDK1.8时间(ns) | 1019 | 1023 | 1188 | 267 | 115 |
2.hash不均匀的时候
次数
|
10
|
100
|
1000
|
10000
|
100000
|
---|---|---|---|---|---|
JDK1.7时间(ns) | 2500 | 14310 | 8151 | 14137 | 154319 |
JDK1.8时间(ns) | 3765 | 38144 | 60707 | 1182 | 373 |
4.2get操作
1.hash比较均匀的时候
次数
|
10
|
100
|
1000
|
10000
|
100000
|
---|---|---|---|---|---|
JDK1.7时间(ns) | 900 | 550 | 627 | 302 | 626 |
JDK1.8时间(ns) | 2773 | 1047 | 318 | 94 | 13 |
2hash不均匀的时候
次数
|
10
|
100
|
1000
|
10000
|
100000
|
---|---|---|---|---|---|
JDK1.7时间(ns) | 2000 | 14950 | 4294 | 2167 | 16447 |
JDK1.8时间(ns) | 3430 | 3932 | 2028 | 767 | 19 |
参考链接:
(1)红黑树的性质:http://blog.csdn.net/cyp331203/article/details/42677833
(2)JDK1.8 HashMap性能的提升:http://blog.csdn.net/lc0817/article/details/48213435/
HashMap JDK 1.8 VS JDK1.7 增加性能相关推荐
- Idea 配置JDK 版本 Idea 配置JDK1.8
Idea 配置JDK 版本 Idea 配置JDK1.8 一.更改项目JDK 1. File --- Project Structue --- Modules --- 设置JDK版本等 2.设定JDK编 ...
- HashMap面试深入详解jdk1.8
HashMap是Java后端工程师面试的必问题,因为其中的知识点太多,很适合用来考察面试者的Java基础.今天基于jdk1.8来研究一下HashMap的底层实现. HashMap的内部数据结构 JDK ...
- Java集合篇:HashMap原理详解(JDK1.8)
概述 JDK 1.8对HashMap进行了比较大的优化,底层实现由之前的"数组+链表"改为"数组+链表+红黑树",本文就HashMap的几个常用的重要方法和JD ...
- Java集合篇:HashMap原理详解(JDK1.7及之前的版本)
(本文有关HashMap的源码都是基于JDK1.6的) 摘要: HashMap是Map族中最为常用的一种,也是 Java Collection Framework 的重要成员.本文首先给出了 Hash ...
- HashMap 源码详细分析(JDK1.8)
1. 概述 本篇文章我们来聊聊大家日常开发中常用的一个集合类 - HashMap.HashMap 最早出现在 JDK 1.2中,底层基于散列算法实现.HashMap 允许 null 键和 null 值 ...
- HashMap 源码详细分析(JDK1.8) 1
1.概述 本篇文章我们来聊聊大家日常开发中常用的一个集合类 - HashMap.HashMap 最早出现在 JDK 1.2中,底层基于散列算法实现.HashMap 允许 null 键和 null 值, ...
- jdk是什么?jdk1.8安装配置方法
2019独角兽企业重金招聘Python工程师标准>>> jdk是什么呢?jdk的是java development kit的缩写,意思是java程序开发的工具包.也可以说jdk是ja ...
- 【JDK】如何实现jdk1.8与jdk11环境的相互切换
最近自己一直在使用的主机快要过期了,公司给换了笔记本,因为不同的项目会使用到不同的 JDK 环境,因此,准备在笔记本上配置下可自由切换的 JDK 环境,在此记录一下. 1.下载和安装JDK1.8和JD ...
- HashMap源码解析——基于JDK1.8
前言 HashMap数据结构由数组和链表(超过一定数量转换为红黑树)组成,在进行增删查等操作时,首先要定位到元素的所在表的位置,之后再从链表中定位该元素.具体找到表下标的方法是(n - 1) & ...
最新文章
- 超棒整理 | Python 关键字知识点大放送
- 46亿一辆的戴森电动车,「卖」给英国首富了
- Winpcap网络编程十之Winpcap实战,两台主机通过中间主机通信
- dos下打包整个java工程
- 【Java1】jdk安装/idea安装,关键字/数据类型/标识符,运算符,/包/类,运算符,if/switch,for/while
- 解决maven项目jdbc报错:java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
- [物理学与PDEs]第1章习题6 无限长载流直线的磁场
- Linux命令_搜索文件
- 7大浏览器颜值代表,谁才是真正的浏览器颜值之王呢?
- SQLyog数据库:主键外键代码添加
- 绿色风格资格证书学校培训机构网站源码 织梦dedecms模板 带手机版
- 1.2GHz Atom处理器 诺基亚N9配置曝光
- C++智能指针入门之unique_ptr
- 能自由转换格式的PDF软件
- 水下自动循迹机器人_一种夜间巡逻机器人自动循迹方法
- 配置线在计算机端 使用什么端口,交换机怎么配置?这几种方式供你使用!
- 财经365内参:万亿赛道迎五部门发利好
- 网络安全(一):常见的网络威胁及防范
- 基于Python实现的二手车价格预测
- python 梦幻西游_GitHub - BestBurning/mhxy: tensorflow实践:梦幻西游人物弹窗识别