1.看源码之前需要了解的一些内容
Node<K,V>[] table // 哈希表结构中数组的名字
DEFAULT_INITIAL_CAPACITY // 数组默认长度16
MAXINUM_CAOACITY // 数组理论最大容量上限,为2的30次方
DEFAULT_LOAD_FACTOR // 默认加载因子0.75
TREEIFY_THRESHOLD // HashMap链表转为红黑树的最小树形化阈值
MIN_TREEIFY_CAPACITY // 允许HashMap的某些链表转为红黑树的最小数组长度阈值
table数组是HashMap底层数据结构中的“数组”,存储链表头或红黑树的root节点
源码中HashMap的构造方法都只是初始化了一下加载因子(loadFactor),没有对底层的数据结构(数组)进行初始化(起码table都没有分配内存) ,从后面put->putVal->resize方法可以得知:在第一次put元素时,才对HashMap对象的数组进行初始化!
HashMap里面每一个对象包含以下内容:
1.1 链表中的键值对对象
包含:
int hash; //键的哈希值
final K key; //键
V value; //值
Node<K,V> next; //下一个节点的地址值
1.2 红黑树中的键值对对象
包含:
int hash; //键的哈希值
final K key; //键
V value; //值
TreeNode<K,V> parent; //父节点的地址值
TreeNode<K,V> left; //左子节点的地址值
TreeNode<K,V> right; //右子节点的地址值
boolean red; //节点的颜色
执行HashMap的put方法时:
//参数一:键
//参数二:值//返回值:被覆盖元素的值,如果没有覆盖,返回null
//参数一:键的哈希值
//参数二:键
//参数三:值
//参数四:如果键重复了是否保留
// true,表示老元素的值保留,不会覆盖
// false,表示老元素的值不保留,会进行覆盖
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
hash方法
//利用键计算出对应的哈希值,再把哈希值进行一些额外的处理
//简单理解:返回值就是返回键的哈希值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
putVal方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//定义局部变量:Node数组tab,Node对象p,整形n和i
Node<K,V>[] tab; Node<K,V> p; int n, i;
/**
* if判断的同时将table赋值给tab,将table的长度赋值给n,这里的table是HashMap的成员变量,表示底层数据结构中的!数组!
* 所以这里是询问是否是首次向hashMap集合中添加元素,是的话就参加扩容方法——resize
*/
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; // resize是扩容方法,局部变量n记录下扩容后的数组容量
/**
*这里的if也在边赋值边判断
*i = (n - 1) & hash 就是JDK17的解决哈希碰撞的策略,是用(数组长度 - 1)去同 节点Key的hash值做 & 运算
*所以JDK17不是hash值同数组长度取余的策略
*/
if ((p = tab[i = (n - 1) & hash]) == null) // 如果要插入节点的下标没有被插入过,就在这里插入
tab[i] = newNode(hash, key, value, null);
else { // 否则
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 如果待插入节点的hash值与当前数组节点的hash相同,并且Key也相同
e = p;
else if (p instanceof TreeNode) // 如果是红黑树,那就要去红黑树的插入方法了,太难了先不看了
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果从数组中获取出来的键值对不是红黑树中的节点
//表示此时下面挂的是链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果e为null,表示当前不需要覆盖任何元素
//如果e不为null,表示当前的键是一样的,值会被覆盖
//这里能看出,HashMap的覆盖策略不是直接替换节点而是覆盖Value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// threshold:记录的就是数组的长度 * 0.75,哈希表的扩容时机 16 * 0.75 = 12
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
resize方法:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // 定义局部变量指向table数组
int oldCap = (oldTab == null) ? 0 : oldTab.length; // oldCap表示旧数组容量(length不是包含元素的数量,是new的那个值或上一次扩容的值)
int oldThr = threshold; // 旧阈值,这里的threshold是门槛,数组扩容的门槛
int newCap, newThr = 0; // 新数组容量,新数组扩容门槛
// 数组如果从未扩容过(这里其实就是数组连初始化都没有),oldCap = oldThr = 0
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
// 很显然,如果第一次执行put方法,前两个if分支都走不了,会到这里
newCap = DEFAULT_INITIAL_CAPACITY; // 数组初始化容量为16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 数组初始扩容门槛为16 * 0.75 = 12
}
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]; // 定义一个空的容量为新容量的Node数组
table = newTab; // table指向新数组
// 下面的操作就是在将老数组上的链表和红黑树复制到新数组上,太复杂了不看了
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;
}