一、传统 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增加了红黑树部分)实现的

hashMap内存结构图

新增红黑树
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 TREEIFY_THRESHOLD = 8
 static final int UNTREEIFY_THRESHOLD = 6
static final int MIN_TREEIFY_CAPACITY = 64
当桶中元素个数超过这个值时
需要使用红黑树节点替换链表节点
当扩容时,桶中元素个数小于这个值
就会把树形的桶元素 还原(切分)为链表结构
当哈希表中的容量大于这个值时,表中的桶才能进行树形化
否则桶内元素太多时会扩容,而不是树形化
为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD

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方法执行过程可以通过下图来理解,自己有兴趣可以去对比源码更清楚地研究学习。

hashMap 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,如果超过,进行扩容。

JDK1.8HashMap的put方法源码
1 public V put(K key, V value) {
 2     // 对key的hashCode()做hash
 3     return putVal(hash(key), key, value, falsetrue);
 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 }

JDK1.7HashMap的put方法源码
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()

JDK1.8中hashMap getNode操作
*/
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.7扩容例图

下面我们讲解下JDK1.8做了哪些优化。经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。

hashMap 1.8 哈希算法例图1

元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

hashMap 1.8 哈希算法例图2

因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:

jdk1.8 hashMap扩容例图

这个设计确实非常的巧妙,既省去了重新计算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 增加性能相关推荐

  1. Idea 配置JDK 版本 Idea 配置JDK1.8

    Idea 配置JDK 版本 Idea 配置JDK1.8 一.更改项目JDK 1. File --- Project Structue --- Modules --- 设置JDK版本等 2.设定JDK编 ...

  2. HashMap面试深入详解jdk1.8

    HashMap是Java后端工程师面试的必问题,因为其中的知识点太多,很适合用来考察面试者的Java基础.今天基于jdk1.8来研究一下HashMap的底层实现. HashMap的内部数据结构 JDK ...

  3. Java集合篇:HashMap原理详解(JDK1.8)

    概述 JDK 1.8对HashMap进行了比较大的优化,底层实现由之前的"数组+链表"改为"数组+链表+红黑树",本文就HashMap的几个常用的重要方法和JD ...

  4. Java集合篇:HashMap原理详解(JDK1.7及之前的版本)

    (本文有关HashMap的源码都是基于JDK1.6的) 摘要: HashMap是Map族中最为常用的一种,也是 Java Collection Framework 的重要成员.本文首先给出了 Hash ...

  5. HashMap 源码详细分析(JDK1.8)

    1. 概述 本篇文章我们来聊聊大家日常开发中常用的一个集合类 - HashMap.HashMap 最早出现在 JDK 1.2中,底层基于散列算法实现.HashMap 允许 null 键和 null 值 ...

  6. HashMap 源码详细分析(JDK1.8) 1

    1.概述 本篇文章我们来聊聊大家日常开发中常用的一个集合类 - HashMap.HashMap 最早出现在 JDK 1.2中,底层基于散列算法实现.HashMap 允许 null 键和 null 值, ...

  7. jdk是什么?jdk1.8安装配置方法

    2019独角兽企业重金招聘Python工程师标准>>> jdk是什么呢?jdk的是java development kit的缩写,意思是java程序开发的工具包.也可以说jdk是ja ...

  8. 【JDK】如何实现jdk1.8与jdk11环境的相互切换

    最近自己一直在使用的主机快要过期了,公司给换了笔记本,因为不同的项目会使用到不同的 JDK 环境,因此,准备在笔记本上配置下可自由切换的 JDK 环境,在此记录一下. 1.下载和安装JDK1.8和JD ...

  9. HashMap源码解析——基于JDK1.8

    前言 HashMap数据结构由数组和链表(超过一定数量转换为红黑树)组成,在进行增删查等操作时,首先要定位到元素的所在表的位置,之后再从链表中定位该元素.具体找到表下标的方法是(n - 1) & ...

最新文章

  1. 超棒整理 | Python 关键字知识点大放送
  2. 46亿一辆的戴森电动车,「卖」给英国首富了
  3. Winpcap网络编程十之Winpcap实战,两台主机通过中间主机通信
  4. dos下打包整个java工程
  5. 【Java1】jdk安装/idea安装,关键字/数据类型/标识符,运算符,/包/类,运算符,if/switch,for/while
  6. 解决maven项目jdbc报错:java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
  7. [物理学与PDEs]第1章习题6 无限长载流直线的磁场
  8. Linux命令_搜索文件
  9. 7大浏览器颜值代表,谁才是真正的浏览器颜值之王呢?
  10. SQLyog数据库:主键外键代码添加
  11. 绿色风格资格证书学校培训机构网站源码 织梦dedecms模板 带手机版
  12. 1.2GHz Atom处理器 诺基亚N9配置曝光
  13. C++智能指针入门之unique_ptr
  14. 能自由转换格式的PDF软件
  15. 水下自动循迹机器人_一种夜间巡逻机器人自动循迹方法
  16. 配置线在计算机端 使用什么端口,交换机怎么配置?这几种方式供你使用!
  17. 财经365内参:万亿赛道迎五部门发利好
  18. 网络安全(一):常见的网络威胁及防范
  19. 基于Python实现的二手车价格预测
  20. python 梦幻西游_GitHub - BestBurning/mhxy: tensorflow实践:梦幻西游人物弹窗识别

热门文章

  1. 一位十年的老司机告诉你什么是编程思想
  2. 给大家介绍几个PDF电子书下载地址(工具辅导类)
  3. android-------- 常用且应该学习的框架
  4. Android附近基站+Wifi+IP+GPS多渠道定位方案
  5. 软件显示 损坏的图像
  6. 骨传导耳机哪个音质好,音质好的骨传导蓝牙耳机介绍
  7. 【黑苹果】联想拯救者Y70002019PG0
  8. Node对象的insertBefore方法
  9. chrome插件(尖兵一号)实现自动刷新淘宝m站的cookie
  10. 蓝桥杯算法之核桃的数量