Java集合源码(一):HashMap
为了方便记录自己所学,决定将笔记记录到博客上,其中有疏漏之处,请大家批评指正。
本系列所涉及源码版本为JDK13,但万变不离其宗,不管哪个版本的源码,其底层思想大同小异。
这里写目录标题
1 概述
- HashMap实现了Map接口,即允许放入key为null的元素,也允许插入value为null的元素;
- HashMap基于key值得hash码查找元素,效率极高,时间复杂度为O(1);
- 与Hashtable不同的是,HashMap是非线程安全的;
- HashMap不保证元素的顺序排列,根据需要该容器可能会对元素重新哈希,元素的顺序也会被重新打散,因此不同时间迭代同一个HashMap的顺序可能会不同。
- 根据对冲突的处理方式不同,哈希表常用的有两种实现方式,一种是开放定址法(Open addressing),另一种是冲突链表方式(Separate chaining with linked lists),HashMap采用的是冲突链表方式。
2 HashMap实现
HashMap的底层数据结构是一个Node节点数组,是数组+链表/红黑树的复合结构,数组中的每个元素都是一个链表,链表的每个节点中存储着我们的Key-Value对。这种数组+链表的加粗样式复合结构综合了数组和链表的优点,使得HashMap在增删改查时最优都能达到O(1)的时间复杂度。
Node节点在HashMap中的实现
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
2.1 属性
// HashMap的底层数据结构,Node节点数组
transient Node<K,V>[] table;
//
transient Set<Map.Entry<K,V>> entrySet;
// HashMap中的键值对个数
transient int size;
// 影响HahsMap结构的修改次数,主要用于fail-fast机制,
// 抛出并发修改异常ConcurrentModificationExcepton
transient int modCount;
// 扩容阈值:capacity * load factor
int threshold;
// 负载因子,默认值为0.75
final float loadFactor;
HashMap中有两个属性可以影响HashMap的性能: 分别是inital capacity和load factor。inital capacity指定了HashMap的初始容量,load factor用来指定当table中元素数量占数组长度的多少时进行扩容,默认为0.75,即3/4。
当Node的数量超过扩容阈值threshold(capacity*load_factor)时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。
2.2 构造器
HashMap有4个构造函数,主要的构造函数是HashMap(int initialCapacity, float loadFactor),可以通过指定初始容量和负载因子初始化HashMap,其他两个构造函数都调用了它。
// 指定初始容量和负载因子初始化HashMap
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
// 指定初始容量初始化HashMao
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 默认构造函数,负载因子默认为0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 容器初始大小,默认为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 容器负载因子默认为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
通过观察默认构造函数,我们可以发现,HashMap在默认初始化时,initialCapacity的初始值为16,loadFactor的初始值为0.75。
2.3 HashMap的存取
2.3.1put()方法
// 添加键值对
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// HashMap存储键值对的实现
// hash:key的hash值
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果HashMap表为空或长度为0,即数组中没有存储元素,则为该数组分配内存空间
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算key在数组table中的索引,判断该处索引存储的链表是否为
// 空,如果为空则直接在该索引处构造一个新的包含键值对的Node节点
// (n - 1) & hash等价于 hash % key
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 如果该索引处已经存在Node节点,进行下一步判断
else {
Node<K,V> e; K k;
// 如果该索引处链表的头节点的hash值和key值与要插入的key-value相同,则直接
// 覆盖头节点
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);
// 上述情况都不符合,则说明该链表要么不包含与要插入的key-value对的key值相
// 同的Node节点,要么该节点在链表的头结点之后
// 直接进行遍历链表操作,查找key值相同的节点
else {
for (int binCount = 0; ; ++binCount) {
// 遍历到尾部还没找到,直接在尾部插入一个新节点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果链表长度超过8,将其转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 找到了,直接break
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果存在key值相同的Node节点,则覆盖其value值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果size大于threshold,扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
HashMap插入键值对过程:
- 通过计算hash(key) % table.length得到要插入的key-value对在数组table中的索引,判断该处索引存储的链表是否为空,如果为空则直接在该索引处构造一个新的包含键值对的Node节点,不为空执行步骤2;
- 判断该处链表的头节点的key’值是否与要插入的key-value对的key值相同,相同则直接覆盖头节点,不同执行步骤3;
- 判断该索引处存储的链表是否已经转化为红黑树,如果是,调用*((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)*方法,不是,执行步骤四;
- 上述情况都不符合,则说明该链表要么不包含与要插入的key-value对的key值相同的Node节点,要么该节点在链表的头结点之后,直接进行遍历链表操作,查找key值相同的节点;
- 1)若再遍历过程中发现链表长度超过8,则将该链表转化为红黑树
2)如果在该链表中找到可以值相同的Node节点,直接覆盖其value值,否则执行步骤2);
3)找不到直接在该链表尾部插入新节点。 - put完毕后,如果table中的元素数量超过扩容阈值(size > threshold),则进行自动扩容并重新哈希。
2.3.2 get(Object)方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// get方法实现,根据key和其hash值查找对应节点
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
get(Object)流程:
- 计算key在table中的索引,如果该处索引存在非空节点,先判断头节点是否是要找的节点,是就直接返回其value值,否则执行步骤2;
- 判断索引处的数据结构,是红黑树就调用红黑树的查询方法getTreeNode();是链表就进行遍历,得到并返回对应节点的value值;
- 找不到直接返回null。
HashMap存取值总结:
1. HashMap基于key的hash码来计算key在table中的索引,如果key-value存储在链表的头结点中,那么key-value对的增删改查的时间复杂度为O(1)!
2. HashMap与ArrayList一样,都会在添加元素时进行自动扩容操作;
3. 在初始化HashMap时,如果可以估计到使用的元素数量,则可以为HashMap指定更大的初始容量,这样既可以避免容器的自动扩容操作,也可以提高HashMap的CRUD效率(表的长度越长,那么就会有更多的元素分布在头结点上)。
4. 如果链表长度超过8,链表会转化为红黑树。
2.4 自动扩容
前文提到,当table中的元素数量达到扩容阈值时(size > threshold = capacity*load_factor),HashMap会进行自动扩容操作。
if (++size > threshold)
resize();
扩容的核心函数是resize(),数组每次扩容至原来的2倍
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 若table长度大于0,分两种情况
// 1. table长度大于MAXIMUM_CAPACITY,无法扩容,将扩容阈值threshold置为
// Integer.MAX_VALUE,直接返回原table数组
// 2. table长度小于MAXIMUM_CAPACITY,则将扩容后新的数组长度newCap和新的扩容阈
// 值newThr置为原来的2倍
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果经上述计算后新的扩容阈值newThr仍然为0,则用扩容后的数组长度乘以负载因子
// 计算出新的扩容阈值(newThr = newCap * loadFactor),该阈值小于等于
// Integer.MAX_VALUE
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 遍历旧的Node数组,将节点重新hash后装入新数组中
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
自动扩容流程:
- 先判断HashMap中的table长度是否大于0,大于0则将扩容后的数组长度newCap和扩容阈值newThr置为原来的2倍,最大不能超过Integer,MAX_VALUE,如果table长度小于0进入步骤2;
- 如果旧的扩容阈值oldThr大于0,则直接将新数组长度newCap置为oldThr的值,否则进入步骤3;
- 上述情况都不满足,直接对newCap和newThr进行默认初始化,newCap = 16, newThr = 0.75;
- 将HashMap的扩容阈值 threshold 置为 newThr,为HashMap新建一个长度为newCap的table数组,并把就数组中的元素重新hash装入新数组中。
总结:
-
当元素数量超过扩容阈值时,HashMap的数组长度和扩容阈值每次扩容至原先的2倍。
-
由于HashMap采用的hash方式是除留取余法(index = hash(key) % table.length),因此随着扩容操作后table数组长度的改变,元素对应的hash结果即在数组中的索引值也会与扩容之前不同,这就是HashMap无法保证元素顺序存放的原因。
2.5 HashMap与红黑树
通过HashMap.put()方法的源码我们知道,当链表长度大于8时,HashMap会将链表转化为红黑树,那么为什么要这样做呢?使用数组+链表的结构难道还不够优秀吗?
我们都知道,链表的增删操作的时间复杂度是O(1)的,但在其上的查询操作的时间复杂度是O(n)的,想象一下,随着链表的长度增加,我们的查询效率将会越来越低,因此JDK1.8对HashMap的底层实现进行了优化,引入了新的数据结构红黑树。
红黑树的本质就是一颗弱平衡的二叉搜索树,查询的时间复杂度可以达到O(logN),效率非常之高。
红黑树特性:
- 每个节点不是黑色就是红色;
- 根节点是黑色。
- 叶子节点都是黑色,且都为NULL。
- 如果一个节点是红色的,则它的父、子节点必须是黑色的。
- 任意一结点到每个叶子结点的路径都包含数量相同的黑结点。
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;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
向平衡二叉搜索树中插入节点,就必然会改变其平衡状态,因此就需要在插入元素后进行左旋、右旋操作维持其平衡状态,红黑树也不例外。
2.5.1 红黑树的查询
红黑树是一颗二叉搜索树,因此,查询操作与二叉搜索树大致相同,从树的根节点开始,根据节点的hash码和key值在红黑树中查询节点:
- 如果待查询节点的hash码和key值与当前节点相等,直接返回当前节点;
- 如果待查询节点的值大于当前节点,进入右子树查询;
- 如果待查询节点的值小于当前节点,进入左子树查询;
- 重复步骤1、2、3,直到找到节点;
- 遍历完整颗红黑树还没找到,返回NULL值。
// 红黑树获取查询节点操作
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
// 红黑树查询操作具体实现
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
2.5.2 红黑树的插值
红黑树的插入主要分两步,1)先找到插入位置,2)插入后自平衡。
插入的节点必须是红色,否则就会违红黑树特性5,需要做自平衡操作: 插入节点为红色,在其父结点(如果存在)为黑色结点时,红黑树的黑色平衡没被破坏,不需要做自平衡操作。但如果插入结点是黑色,那么插入位置所在的子树黑色结点总是多1,必须做自平衡。这也是为了插入效率考量,尽量减少红黑树的自平衡操作。
插入的函数实现putTreeVal(),返回插入后的红黑树:
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
红黑树插值流程:
- 如果树的根节点为NULL,直接新建一个节点并返回,否则就遍历红黑树;
- 遍历红黑树,如果当前已经存在key值相同的节点,则直接替换其value;
- 否则查找到待插入位置,将节点插入到该位置,并调用balanceInsertion()对红黑树进行自平衡。
balanceInsertion()实现对插入节点后的红黑树的自平衡,关于红黑树的具体原理以及自平衡操作有一篇文章《30张图带你彻底理解红黑树 - 枯木逢春 - 博客园》讲的很好,我就不班门弄斧了,感兴趣的同学可以去阅读。
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
// 将待插入节点置为红色
x.red = true;
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
// 如果
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
else if (!xp.red || (xpp = xp.parent) == null)
return root;
if (xp == (xppl = xpp.left)) {
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.right) {
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
else {
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
2.5.3 红黑树的建树操作
红黑树的建树操作主要是由TreeNode节点类的treeify()函数完成的,实际就是红黑树插值的循环操作。
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null{
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
建树流程:
- 首先基于二叉搜索树的性质将链表中的节点依次插入到新建的红黑树中;
1)在红黑树中找到要插入的位置;
2)将节点插入到该位置;
3)对插入后的红黑树进行自平衡操作;
4)重复上述操作,直到将链表中的节点插入完毕。
2.6 HashMap常用方法
1.添加元素
编号 | 存值 |
---|---|
1 | V put(K key, V value) |
2 | void putAll(Map<? extends K, ? extends V> m) |
3 | V putIfAbsent(K key, V value) |
2.获取元素
编号 | 取值 |
---|---|
1 | V get(Object key) |
2 | V getOrDefault(Object key, V defaultValue) |
在这里想提一下getOrDefault()方法,这个方法再平常使用的时候还是很方便的。实现的主要思路是设置一个defaultValue,随后在map中查找hash和key值相同的节点,如果找到节点,就返回节点的value值,找不到就返回默认值。
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
return (e = getNode(hash(key), key)) ==
null ? defaultValue : e.value;
}
3.获取键值序列
编号 | 获取键值序列 |
---|---|
1 | Set keySet() |
2 | Collection values() |
3 | Set<Map.Entry<K,V>> entrySet() |
4.删除节点
编号 | 删除 |
---|---|
1 | V remove(Object key) |
2 | void clear() |
5.替换节点value值
编号 | 替换 |
---|---|
1 | V replace(K key, V value) |
2 | void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) |
6.常用的辅助函数
编号 | 辅助函数 |
---|---|
1 | int size() |
2 | boolean isEmpty() |
3 | boolean containsKey(Object key) |
4 | boolean containsValue(Object value) |