HashMap源码深度解析(深入至红黑树实现)以及与JDK7的区别四万字
Posted 刘Java
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap源码深度解析(深入至红黑树实现)以及与JDK7的区别四万字相关的知识,希望对你有一定的参考价值。
基于JDK1.8对HashMap集合的主要方法源码解析,深入至底层红黑树的源码,并且与JDK1.7的HashMap做了比较全面的对比,最后给出了比较完整的HashMap的数据结构图!
本文主要是对JDK1.8的HashMap的主要方法实现做了分析,对于一些基础的知识,认为大家在看这篇文章之前是都懂得的,比如哈希表的原理、红黑树的原理!
如果大家有不了解这些原理的一定要去看看相关文章,否则如果直接看下面的源码的分析肯定有你看不懂的!
数据结构—红黑树(RedBlackTree)的实现原理以及Java代码的完全实现,这是看懂HashMap底层红黑树源码必备的基础知识!
数据结构—散列表(哈希表)的原理以及Java代码的实现,HashMap就是一张散列表,这是关于散列表的介绍!
文章目录
- 1 HashMap的概述
- 2 主要类属性
- 3 主要内部类
- 4 构造器
- 5 put方法
- 6 remove方法
- 7 其他方法
- 8 Hashmap 在JDK1.7和1.8的某些区别
- 9 总结
1 HashMap的概述
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
HashMap,来自于JDK1.2的哈希表的实现,JDK1.8底层是使用数组+链表+红黑树来实现的(JDK1.7是使用数组+链表实现的),使用“链地址法(拉链法)”解决哈希冲突。
实现了Map接口,存放的自然是key-value形式的数据,拥有Map接口的通用操作。允许null键和null值,元素无序。
实现了Cloneable、Serializable标志性接口,支持克隆、序列化操作。
此实现不是同步的,可以使用Collections.synchronizedMap()方法获得一个同步的Map。
默认容量为16,第一次存放元素时初始化;默认加载因子为0.75;扩容增量为增加原容量的1倍,即变成原容量的两倍。
2 主要类属性
主要类属性包括一些默认值常量属性,还有一些关键属性。
从这些属性可知,默认初始容量16,最大容量2^30,加载因子0.75。
链表树形化阈值8(大于),哈希表树形化阈值64(大于等于),resize时树还原阈值6(小于等于)。
/**
* 默认初始容量为16,所有的容量必须是2的幂次方,这在哈希算法中会使用到。
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* 最大容量为1 << 30,即2^30次方。
* 我们知道int类型的范围是[-2^31 ~ 2^31-1],因此这里的2^30实际上就是int范围类的最大的2的幂次方值。
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认加载因子为0.75
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 链表树形化阈值,即链表转成红黑树的阈值,在存储数据时,当链表长度 大于8 时,则将链表转换成红黑树。
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 红黑树还原为链表的阈值,当在扩容时,resize()方法的split()方法中使用到该字段
* 在重新计算红黑树的节点存储位置后,当拆分成的红黑树链表内节点数量 小于等于6 时,则将红黑树节点链表转换成普通节点链表。
* <p>
* 该字段仅仅在split()方法中使用到,在真正的remove删除节点的方法中时没有用到的,实际上在remove方法中,
* 判断是否需要还原为普通链表的个数不是固定为6的,即有可能即使节点数量小于6个,也不会转换为链表,因此不能使用该变量!
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 哈希表树形化的最小容量阈值,即当哈希表中的容量 大于等于64 时,才允许树形化链表,否则不进行树形化,而是扩容。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 底层存储key-value数据的数组,长度必须是2的幂次方。由于HashMap使用"链地址法"解决哈希冲突,table中的一个节点是链表头节点或者红黑树的根节点。节点类型为Node类型,后面我们会分析到,Node的实际类型可能表示链表节点,也可能是红黑树节点。
*/
transient Node<K, V>[] table;
/**
* 集合中键值对的数量,可通过size()方法获取。
*/
transient int size;
/**
* 扩容阈值(容量 x 加载因子),当哈希表的大小大于等于扩容阈值时,哈希表就会扩容。
*/
int threshold;
/**
* 哈希表实际的加载因子。
*/
final float loadFactor;
3 主要内部类
HashMap的内部类比较多,这里讲解主要的内部节点类,一个是Node
节点,即普通链表节点;另一个是TreeNode
节点类,即红黑树节点。
实际上Node节点直接实现了Map.Entry(Map体系中的集合的内部节点实现类的超级接口),实现了Map.Entry接口的全部方法。而TreeNode直接继承了LinkedHashMap.Entry
节点类,而LinkedHashMap类中的节点类Entry
则是继承了Node
节点类。
虽然它们的关系有点绕,但是TreeNode和Node仍然属于Map.Entry节点体系,并且TreeNode节点类通过LinkedHashMap.Entry间接继承了Node节点类(爷孙关系)。因此底层数组table中的Node的实际类型可能就是链表节点Node,也可能是红黑树节点TreeNode。
3.1 Node
/**
* JDK1.8的HashMap的链表节点实现类(JDK 1.7 使用Entry类,只是名字不一样)。
* <p>
* 具有hash属性,用于存放key的hashCode方法的返回值,避免重复计算。
* 具有key、value属性用于存放键值对。
* 具有next属性,由于HashMap使用"链地址法"解决哈希冲突,因此使用next指向后来加入的 存放在同一个桶位置(即哈希冲突)的节点。
* <p>
* 实现了Map.Entry(Map体系中的集合的内部节点实现类的超级接口)。
* 实现了Map.Entry接口的全部方法,比如getKey、getValue、setValue,总之:比较简单。
*/
static class Node<K, V> implements Map.Entry<K, V>
final int hash;
final K key;
V value;
Node<K, V> next;
Node(int hash, K key, V value, Node<K, V> next)
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
public final K getKey()
return key;
public final V getValue()
return value;
public final String toString()
return key + "=" + value;
public final int hashCode()
return Objects.hashCode(key) ^ Objects.hashCode(value);
public final V setValue(V newValue)
V oldValue = value;
value = newValue;
return oldValue;
public final boolean equals(Object o)
if (o == this)
return true;
if (o instanceof Map.Entry)
Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
return false;
3.2 TreeNode
/**
* JDK1.8的HashMap的红黑树节点实现类,直接实现了LinkedHashMap.Entry节点类。
* <p>
* 具有传统红黑树节点该有的属性,比如两个子节点、父节点、节点颜色等。
* 具有链表树化的方法和树还原链表的方法,具有查找、存放、移除树节点的方法,具有调整平衡、左旋、右旋的方法,总之:比较复杂。
* 后面会具体分析它的源码!
*/
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;
//节点的颜色,默认是红色
boolean red;
/**
* 构造方法,实际上只是调用了父类LinkedHashMap.Entry的构造方法,
* 而在LinkedHashMap.Entry的对应构造方法中,又调用父类Node的构造方法
*
* @param hash key的hashCode返回值
* @param key k
* @param val v
* @param next 链表的下一个节点引用
*/
TreeNode(int hash, K key, V val, Node<K, V> next)
super(hash, key, val, next);
/**
* 返回包含此节点的树的根节点
*/
final TreeNode<K, V> root()
//……
/**
* 把给定的节点是设置桶的第一个节点,即数组的节点
*/
static <K, V> void moveRootToFront(Node<K, V>[] tab, TreeNode<K, V> root)
//……
/**
* 从当前节点开始通过给定的hash和key查找节点
*/
final TreeNode<K, V> find(int h, Object k, Class<?> kc)
//……
/**
* 从根节点开始通过给定的hash和key查找节点
*/
final TreeNode<K, V> getTreeNode(int h, Object k)
//……
/**
* 比较节点大小,用来排序
*/
static int tieBreakOrder(Object a, Object b)
//……
/**
* 链表树化
*/
final void treeify(Node<K, V>[] tab)
//……
/**
* 红黑树还原为链表,removeTreeNode、split方法中会调用到
*/
final Node<K, V> untreeify(HashMap<K, V> map)
//……
/**
* 存放树节点
*/
final TreeNode<K, V> putTreeVal(HashMap<K, V> map, Node<K, V>[] tab,
int h, K k, V v)
//……
/**
* 删除树节点
*/
final void removeTreeNode(HashMap<K, V> map, Node<K, V>[] tab,
boolean movable)
//……
/**
* resize时,对红黑树节点的调用方法,包含了untreeify的逻辑
*/
final void split(HashMap<K, V> map, Node<K, V>[] tab, int index, int bit)
//……
/**
* 左旋
*/
static <K, V> TreeNode<K, V> rotateLeft(TreeNode<K, V> root,
TreeNode<K, V> p)
//……
/**
* 右旋
*/
static <K, V> TreeNode<K, V> rotateRight(TreeNode<K, V> root,
TreeNode<K, V> p)
//……
/**
* 插入后调整平衡
*/
static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root,
TreeNode<K, V> x)
//……
/**
* 删除后调整平衡
*/
static <K, V> TreeNode<K, V> balanceDeletion(TreeNode<K, V> root,
TreeNode<K, V> x)
//……
/**
* 递归不变性检查
*/
static <K,V> boolean checkInvariants(TreeNode<K,V> t)
//……
4 构造器
4.1 HashMap()
public HashMap()
构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
/**
* 默认构造函数,可以看到并没有进行底层数组初始化,只是设置了加载因子为默认加载因子,即0.75
*/
public HashMap()
this.loadFactor = DEFAULT_LOAD_FACTOR;
4.2 HashMap(initialCapacity)
public HashMap(int initialCapacity)
构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。如果初始容量为负,则抛出IllegalArgumentException异常。
public HashMap(int initialCapacity)
//内部调用指定容量大小和默认加载因子0.75的构造函数
this(initialCapacity, DEFAULT_LOAD_FACTOR);
4.3 HashMap(initialCapacity, loadFactor)
public HashMap(int initialCapacity,float loadFactor)
构造一个带指定初始容量和加载因子的空 HashMap。如果初始容量为负或者加载因子为非正,则抛出IllegalArgumentException异常。
注意,加载因子可以大于1。
/**
* 构造一个带指定初始容量和加载因子的空 HashMap。
* 如果初始容量为负或者加载因子为非正,则抛出IllegalArgumentException异常。
*/
public HashMap(int initialCapacity, float loadFactor)
/*1 参数检测*/
// 如果指定的初始容量小于0,那么抛出IllegalArgumentException异常
// 指定初始容量必须非负数,否则报错
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 如果指定的初始容量如果大于最大容量,那么初始容量等于最大容量
// 即HashMap的最大容量只能是MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 如果加载因子小于等于0或者不是一个数值类型,那么抛出IllegalArgumentException异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
/*2 初始化参数*/
/*到这一步,说明上面的检测全部通过*/
// 设置实际加载因子
this.loadFactor = loadFactor;
// 设置“扩容阈值”。注意这里设置的值并不是真正的阈值,因为我们说过容量只能是2的幂次方,因此需要先确保容量是2的幂次方才行
// 这里的tableSizeFor方法仅仅只是将传入的容量转化为大于等于该容量的最小2的幂次方,然后赋值给threshold这个变量临时存储真正的初始容量,而真正的阈值在后面还会重新计算
/*2.1 设置真正的初始容量*/
this.threshold = tableSizeFor(initialCapacity);
/**
* 2.1 tableSizeFor(initialCapacity),类似于JDK 1.7 中 inflateTable()里的 roundUpToPowerOf2(toSize),或者类似于JDK1.8 ArrayDeque中的allocateElements(numElements)方法
* 该方法用于将传入的容量转化为大于等于该容量的最小2的幂次方值,即用于计算真正的初始化容量
*/
static final int tableSizeFor(int cap)
// 使用cap-1来计算,是因为下面的5行算法是 尝试查找大于数n的最小2的幂次方-1,因此要想查找大于等于cap的最小2的幂次方,只能使用cap-1来进行运算
int n = cap - 1;
//使用无符号右移和位或操作,尝试将n的最高位1的所有低位全部都变成1,即变成了一个奇数。
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
//上面的算法看起来很完美,但是实际上还是有两个局限性的:
//1 如果cap=0,那么经过上面的算法计算出来的n为-1,小于了0
//2 如果cap> (1<<30) ,那么经过上面的算法计算出来的n为2147483647,即int最大值,大于了最大容量
//因此还需要下面的3个判断:
//1如果n小于0,即如果cap传入0,那么n=0-1=-1,那么返回初始容量1
//2否则,如果n大于等于最大容量,那么就返回最大容量;
//3否则,由于此时n为(大于原n的最小2的幂次方-1),n+1之后正好是大于等于cap的最小2的幂次方,返回n+1。
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
关于上面tableSizeFor的算法更详细解释,我在这篇文章的“ArrayDeque(int numElements)”部份有解释:Java集合—ArrayDeque的源码深度解析以及应用介绍。另外在JDK1.8的Integer类中的highestOneBit方法也使用了类似的算法,不过它是尝试返回小于等于参数的2的幂次方!
4.4 HashMap(m)
public HashMap(Map<? extends K,? extends V> m)
构造包含指定Map的新HashMap。所创建的HashMap具有默认加载因子(0.75)和足以容纳指定 Map 中键值对的初始容量。如果指定的映射为 null,则抛出NullPointerException异常。
/**
* 构造包含指定Map的新HashMap。
* 所创建的HashMap具有默认加载因子(0.75)和足以容纳指定 Map 中键值对的初始容量。
*/
public HashMap(Map<? extends K, ? extends V> m)
//设置加载因子为默认值0.75
this.loadFactor = DEFAULT_LOAD_FACTOR;
//1 将传入的子Map中的全部元素逐个添加到HashMap中
putMapEntries(m, false);
/**
* 1 将传入的子Map中的全部元素逐个添加到HashMap中
* @param m 被加入的集合
* @param evict 在构造器中调用该方法时传入false,其他方法中(比如put、putAll)调用该方法时传入true。实际上在HashMap中没啥用,是留给其子类linkedHashMap用于实现LRU缓存的!
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict)
//s为m的实际元素个数
int s = m.size();
if (s > 0)
// 判断table是否已经初始化
if (table == null) // pre-size
// 未初始化,计算初始容量
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 计算得到的t大于阈值,则初始化阈值
if (t > threshold)
threshold = tableSizeFor(t);
// 已初始化,并且m元素个数大于阈值,进行扩容处理
else if (s > threshold)
resize();
// 然后,将m中的所有元素循环添加至HashMap中
for (Map.Entry<? extends K, Java 1.8 HashMap源码解析 桶数组+单链表+红黑树