本文介绍的内容不包含红黑树的操作
1. 介绍
- 底层是数组 + 链表(JDK 1.8后是红黑树,当链表长度大于8时,链表转换为红黑树)
2. 源码
2.1 常量
-
默认容量大小 DEFAULT_INITIAL_CAPACITY = 16
-
数组最大长度 MAXIMUM_CAPACITY = 2^30
-
默认负载因子 DEFAULT_LOAD_FACTOR = 0.75
-
树化(某一条链表升级为红黑树)的阈值 TREEIFY_THRESHOLD = 8
前提是所有元素数量达到 MIN_TREEIFY_CAPACITY = 64 后
-
树降级称为链表的阈值 UNTREEIFY_THRESHOLD = 6
2.2 属性
// 哈希表
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
// 哈希表的元素个数
transient int size;
// 哈希表的结构修改次数,如插入、删除(替换不算)
transient int modCount;
// 扩容阈值,当哈希表的元素超过阈值时,触发扩容
// 初始化为一个大于等于cap的值,且该值为2的幂(cap是调构造方法时的传参)
int threshold;
// 负载因子,threshold = capacity(当前数组的大小) * loadFactor
final float loadFactor;
2.3 构造方法
- 构造方法里调用了
int tableSizeFor(int cap)方法
// 构造方法传进来的容量大小可以是任意数,但该方法保证了初始化的大小为2的幂
// 返回一个大于等于cap的值,且该值为2的幂
static final int tableSizeFor(int cap) {
int n = cap - 1;
// >>:带符号右移。正数右移高位补0,负数右移高位补1
// >>>:无符号右移。无论是正数还是负数,高位通通补0
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
2.4 put方法
- put之前先调用hash()方法计算key的哈希值,然后再进行路由寻址:(数组长度 - 1) & 哈希值
static final int hash(Object key) {
int h;
// key的哈希值 异或 它的高16位
// 当数组的长度很短时,只有低位数的哈希值能参与运算;而让高16位参与运算可以更好的均匀散列,减少碰撞,进一步降低hash冲突的几率,并且使得高16位和低16位的信息都被保留了。
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- put方法里会调用putVal方法
/**
* Implements Map.put and related methods.
*
* @param hash key的哈希值
* @param key key
* @param value value
* @param onlyIfAbsent 如果为true,则不替换key相同的value(put方法调用时传入fasle,则会替换)
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// 当前Map的散列表
HashMap.Node<K, V>[] tab;
// 当前元素
HashMap.Node<K, V> p;
// n:散列表的长度
// i:路由寻址结果的下标
int n, i;
// 第一次调用putVal时才初始化Map中最耗内存的散列表
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 最简单的场景,寻址找到的位置没有元素,则直接创建元素放入散列表
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 临时元素,最终会指向key相同的旧元素
HashMap.Node<K, V> e;
// 临时key
K k;
// 此时p指向该位置上的旧元素(链表的头节点 或 红黑树的根节点)
// 如果该位置上的旧元素与新元素的哈希值相同,并且两者的key相同,则把e指向旧元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果旧元素已经形成红黑树
else if (p instanceof HashMap.TreeNode)
e = ((HashMap.TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
// 此时链表的头节点不等于新元素,则需要逐个比较链表上是否有key相同的旧元素,没有则在末尾添加
else {
for (int binCount = 0; ; ++binCount) {
// 遍历到最后一个元素,也没有key相同的元素,则在链表末尾添加新元素
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果当前链表长度超过树化阈值,则转成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 如果找到key相同的元素,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 此时e指向key相同的旧元素
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 散列表修改结构的次数加一
++modCount;
// 插入新元素,size自增,如果大于扩容阈值,则触发扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
2.5 resize扩容方法
- 为什么需要扩容:为了解决哈希冲突导致的链化影响查询效率的问题,扩容可以缓解该问题
final Node<K,V>[] resize() {
// 扩容前旧的哈希表
Node<K,V>[] oldTab = table;
// 扩容前旧的table数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 扩容前旧的扩容阈值
int oldThr = threshold;
// newCap:扩容后table数组的大小
// newThr:扩容后,下次再触发扩容的阈值
int newCap, newThr = 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
}
// 此时oldCap = 0,说明还未初始化
// 1. new HashMap(initCap, loadFactor);
// 2. new HashMap(initCap);
// 3. new HashMap(tempMap); 且tempMap有内容
// 这三种情况把旧扩容阈值赋给新容量,newCap一定是2的幂
else if (oldThr > 0)
newCap = oldThr;
// 此时oldCap = 0, oldThr = 0
// new HashsMap();
else {
// 此时新容量为16
newCap = DEFAULT_INITIAL_CAPACITY;
// 新扩容阈值为 0.75 * 16 = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
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;
if (oldTab != null) {
// j:扩容前旧的寻址地址
for (int j = 0; j < oldCap; ++j) {
// 当前node节点元素
Node<K,V> e;
// 当前桶位有数据,但是无法区分是单节点,还是链表,或者还是红黑树
if ((e = oldTab[j]) != null) {
// 置为null让GC回收
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);
// 上述两种情况不满足,此时就是链表,见章节2.5.1
else {
// 低位链表:扩容前与扩容后的寻址地址一样的
Node<K,V> loHead = null, loTail = null;
// 高位链表:扩容后的寻址地址 = 扩容前的寻址地址 + 扩容前数组长度
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 下面的if/else的操作是把元素放到 低位链表 或 高位链表 中
// (e.hash & oldCap) == 0 说明这些元素应放到低位链表中
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);
// 扩容后,低位链表仍然放到旧的寻址地址j里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 扩容后,高位链表放到新的寻址地址里
// 新的寻址地址 = 旧的寻址地址 + 扩容前数组长度
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
2.5.1 扩容时链表的处理
2.6 get方法
- get方法里会调用getNode方法
/**
* Implements Map.get and related methods.
*
* @param hash key的哈希值
* @param key key
* @return 返回key-value节点,若无则返回null
*/
final Node<K,V> getNode(int hash, Object key) {
// 散列表数组
Node<K,V>[] tab;
// first:桶位头元素
// e:临时node节点元素
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 &&
((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;
}
2.7 remove方法
- remove方法里会调用removeNode方法
/**
* Implements Map.remove and related methods.
*
* @param hash key的哈希值
* @param key key
* @param value value
* @param matchValue 为true时还要考虑value是否相同
* @param movable 为false则在删除时,不移动其他节点,只影响红黑树
* @return 返回被删除的节点,若无返回null
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
// 散列表数组
Node<K,V>[] tab;
// 当前node节点元素
Node<K,V> p;
// n:散列表数组长度
// index:寻址结果
int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
// 执行到这说明当前桶位有数据,需要进行查找操作并删除
// node:待删除元素
// e:当前元素p的下一个元素
Node<K,V> node = null, e;
K k;
V v;
// (情况一)如果桶位第一个元素就是待删除元素,直接赋给node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// (情况二)如果时红黑树,则走红黑树的查找逻辑
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// (情况三)否则遍历链表,找到待删元素,赋给node
// 结束循环后,p是node的前一个节点
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 找到待删元素node,在matchValue为true的时候考虑value是否相等
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 对应情况二,如果是红黑树,走红黑树的删除逻辑
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 对应情况一,直接把node.next放在桶位头节点
else if (node == p)
tab[index] = node.next;
// 对应情况三,此时p是node的前一个节点,所以直接进行以下操作
else
p.next = node.next;
// 哈希表的结构修改次数加一,哈希表大小减一
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
2.8 replace方法
/**
* 获取待替换的元素,返回旧的value
*/
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}