JDK17中HashMap的源码:【好恶心,居然不能用插入代码的方式写代码】
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
// 初始容量: 2的4次方 -> 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量: 2的30次方 -> 1073741824
static final int MAXIMUM_CAPACITY = 1 << 30;
// 如果没有指定扩容因子,默认 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 使用树而不是列表的bin计数阈值。当向至少有这么多节点的桶中添加元素时,桶将转换为树。该值必须大于2,并且应该至少为8。
static final int TREEIFY_THRESHOLD = 8;
// 在调整大小操作期间取消树化(拆分)bin的bin计数阈值。应小于TREEIFY_THRESHOLD,且最多为6,以便在移除时进行收缩检测。
static final int UNTREEIFY_THRESHOLD = 6;
// 表,在第一次使用时初始化,并根据需要调整大小。在分配时,长度总是2的幂。
transient Node<K,V>[] table;
// 保存缓存的entrySet()。注意,AbstractMap字段用于keySet()和values()。
transient Set<Map.Entry<K,V>> entrySet;
// 基本哈希bin节点,用于大多数条目。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// 省略...
}
// 从内部类TreeNode可以得知,典型的树结构
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;
// 省略...
}
}
可以从以下几个方面去读:
1. HashMap的存储结构是怎样的?
2. HashMap的put()方法是如何工作的?
3. HashMap是如何扩容的?
Q1:HashMap的存储结构是怎样的?
由源码可知:
在HashMap中,数据存储一共分为两种结构:
- 链表散列:数组+链表
- 红黑树结构
哈希表结构(链表散列:数组+链表)实现,结合数组和链表的优点。当链表长度超过 8 时,链表转换为红黑树。transient Node<K,V>[] table;
Q2:HashMap的put()方法是如何工作的?
HashMap 底层是 hash 数组和单向链表实现,数组中的每个元素都是链表,由 Node 内部类(实现 Map.Entry<K,V>接口)实现,HashMap 通过 put & get 方法存储和获取。
存储对象时,将 K/V 键值传给 put() 方法:
1. 调用 hash(K) 方法计算 K 的 hash 值,然后结合数组长度,计算得数组下标;
2. 调整数组大小(当容器中的元素个数大于 capacity * loadfactor 时,容器会进行扩容resize 为 2n);
3. i.如果 K 的 hash 值在 HashMap 中不存在,则执行插入,若存在,则发生碰撞;
ii.如果 K 的 hash 值在 HashMap 中存在,且它们两者 equals 返回 true,则更新键值对;
iii. 如果 K 的 hash 值在 HashMap 中存在,且它们两者 equals 返回 false,则插入链表的尾部(尾插法)或者红黑树中(树的添加方式)。
【注意:当碰撞导致链表大于 TREEIFY_THRESHOLD = 8 时,就把链表转换成红黑树】
Q3:HashMap是如何扩容的?
创建一个新的数组,其容量为旧数组的两倍,并重新计算旧数组中结点的存储位置。结点在新数组中的位置只有两种,原下标位置或原下标+旧数组的大小。