一、前言
在HashMap【源码分析-JDK1.7 版本】中,有介绍JDK1.7版本的底层源码,那么本文主要介绍JDK1.8版本的源码。
二、源码详细讲解
(一)常量与默认值
1 | public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { |
(二)构造方法
1、构造方法1
1 | public HashMap(int initialCapacity, float loadFactor) { |
2、tableSizeFor
1 | // 将创建HashMap时指定的期望容量(其实就是哈希表中数组的长度)转换为2的幂次方 |
3、构造方法2
1 | public HashMap(int initialCapacity) { |
4、构造方法3
1 | public HashMap() { |
5、构造方法4
1 | public HashMap(Map<? extends K, ? extends V> m) { |
6、putMapEntries
1 | // 将m中的所有元素添加至本HashMap中 |
(三)Put
1、put
1 | public V put(K key, V value) { |
2、hash
1 | // 求当前key的hash值 |
3、putVal
putVal的整体思路如下:
1)在添加元素之前,先判断数组是否已经初始化或者数组是否需要扩容(resize不仅仅是扩容,还需要完成初始化的任务)
2)如果已经初始化或者不需要扩容,那么久执行添加任务,但又分为以下4种情况:
a. 对应的数组index位置还没有元素,那么直接添加;
b. 对应的数组index位置已经有元素,但是要判断是否在链表的第一个位置,即:是否存在数组中;
c. 对应的数组index位置已经有元素,但是要判断该元素是否为红黑树的一个节点;
d. 对应的数组index位置已经有元素,但是要判断是否为链表的一个节点。
1 | final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { |
4、resize
1 | // 扩容 |
5、putTreeVal
该方法属于红黑树如何插入节点的问题,和HashMap的关系不大。
暂时没有仔细研究,目前我们只需要知道它会将一个节点插入到红黑树中即可。
6、treeifyBin
1 | final void treeifyBin(Node<K,V>[] tab, int hash) { |
7、treeify
该方法的解析请参考:JDK8:HashMap源码解析:put方法
(四)Get
1、get
1 | public V get(Object key) { |
2、hash
1 | // 求当前key的hash值 |
3、getNode
1 | // 分为三种情况: |
三、结构流程图
(一)Put过程

若图片不清晰,请点击在线查看
(二)resize方法

若图片不清晰,请点击在线查看
(三)Get过程

若图片不清晰,请点击在线查看
四、补充知识点
1、都知道在 JDK1.8 中 HashMap 的底层数据结构是:数组+链表+红黑树,那为什么要使用这样的数据结构呢?
1)JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到数组中时,可能会导致数组某个位置下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。
2)为什么使用红黑树却不使用别的数据结构?
因为不仅仅是要考虑插入时的性能,也要保证查找的性能,红黑树其实就是一种折中的实现方式。
ps:关于红黑树的介绍、HashMap中红黑树的操作,可以参考以下文章
2、和 JDK1.7 中的 hash 方法相比,为什么 JDK1.8 中的 hash 方法更简单呢?
其实 1.7 中的 hash 方法更麻烦是因为它为了提高 HashMap 的散列性,多次使用了移位运算和异或运算,让hash值的高位也参与运算,从而提高获取元素时的性能;
而在 1.8 中,引入了红黑树,这个时候即使散列性没有那么好,但因为链表不会很长,所以其实查询的效率不会低,因此可以简化hash方法。
3、关于数组的扩容与链表转换为红黑树
在 putVal() 方法中,我们看到在使用尾插法往链表中插入节点后,有如下3行代码:
1
2
3 p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);这三行代码其实是在插入节点后,判断当前链表的节点数是否大于等于8
如果满足条件就会调用 treeifyBin(tab, hash) 方法,也就是所谓的将链表转换为红黑树,
但是如果我们跟进这个方法的源码就会发现有如下代码:
1
2 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();if(第一个条件是判断数组是否还未初始化)的第二个条件其实是在看当前数组中的元素个数是否小于 64,
如果小于64的话其实执行的是扩容操作,而不是将链表转换为红黑树。
所以就有以下结论:
1)当前链表长度>=8,且数组中元素的个数>=64:将链表转换为红黑树;
1)当前链表长度>=8,但数组中元素的个数<64:执行扩容操作。
ps:关于结论2的补充,为什么<64就只执行扩容操作呢?
1)扩容和树化的本质都是为了防止单一链表过长,从而导致查询效率低,而我们在进行扩容之后也能有效的降低链表长度;
2)在扩容之后其实也会有更多的空间来进行存储,这样其实是基于一个更长远的考虑。
ps:我们都知道链表长度>=8时,会判断是否需要转换为红黑树,但是有没有想过为什么这个阈值是8呢?
对于这个问题,可以参考以下文章:
4、关于将红黑树转换为链表的操作
其实不仅仅是有将链表转换为红黑树的操作,也有将红黑树转换为链表的操作。
在源码中我们会发现有这样一个常量:
1 static final int UNTREEIFY_THRESHOLD = 6;这个常量其实就是用来判断,红黑树的节点数是否小于等于6,如果满足这个条件就会将红黑树转换为链表。
但是这个值为什么设置为6,而不是其它数,比如7呢?
其实这是为了避免树与链表之间转换太频繁,从而影响效率。
假设这个值设置为7,并且此时一个链表已经有8个节点,那么会转换为红黑树,但是如果之后又删除了一个节点的话,又满足去树化的条件,红黑树又会转换为链表,如果这样的操作重复出现,就会导致两者之间切换太频繁,从而影响效率。
5、在 JDK1.8 中为什么将头插法改成了尾插法?
在 JDK1.8 中,因为引进了红黑树,从而需要判断链表当前的元素个数,所以需要遍历链表进行统计。
那么既然都已经遍历到链表的末尾了,为什么不直接在尾部插入呢?
当然这不是最主要的原因,最根本的原因如下:
因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
并且在整个遍历的过程中,其实一共是完成了三件事情:
1)统计链表上的元素个数;
2)需要判断要插入的元素的key,是否在当前链表中已经存在;
3)遍历到尾部后,使用尾插法插入元素。
另:以下文章写得很好,推荐阅读
Java源码分析:HashMap 1.8 相对于1.7 到底更新了什么?
Java新手,若有错误,欢迎指正!