一、知识储备
(一) HashMap 继承体系
仅提供我们需要关心的几个map关系
(二) HashMap 数据结构
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
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;
}
HashMap存储数据都是由Node负责的,每一个Node都存储了一对键值对key-value;
并且它关联了下一个next节点,从这里就能看到单链表的影子了。
HashMap随着容量(size)和hash碰撞的次数不断增长,HashMap的结构也在处于不同状态:
- 当它的存入的节点Node未发生hash碰撞时候,HashMap只是一个普通的数组;
- 发生hash碰撞,所在节点发生**
链化
**,节点Node升级为链表,hash值相同的节点相继存入这个链表中;- 当散列表(table)size > 64,链的长度超过8,该链发生树化,链表转化为红黑树
(三) Hash原理,Hash碰撞,以及链表中的hash值
1. ▲
Hash原理
Hash(散列函数-百度百科)也称散列、哈希,对应的英文都是Hash。 基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出。 这个映射的规则就是对应的Hash算法,而原始数据映射后的二进制串就是哈希值。 … 整个Hash算法的过程就是把原始任意长度的值空间,映射成固定长度的值空间的过程。
▲
Hash的特点:
- 从hash值不可以反向推导出原始的数据;
- 输入数据的微小变化会得到完全不同的hash值,相同的数据会得到相同的值;
- 哈希算法的执行效率要高效,长的文本也能够很快计算出哈希值;
- hash算法的冲突概率要小
2. ▲
Hash碰撞(哈希碰撞)
由于hash算法的原理是将输入空间的值映射称hash空间内的值,而hash值的空间的远远小于输入的空间。根据抽屉原理,一定会存在不同的输入被映射成相同的输出的情况,即哈希碰撞。
▲抽屉原理
抽屉原理的一般含义为:“如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有n+1个元素放到n个集合中去,其中必定有一个集合里至少有两个元素。” 抽屉原理有时也被称为鸽巢原理。 它是组合数学中一个重要的原理 。
▲
因为存在哈希碰撞,所以不能用hash值来区分对象的唯一性,但是却可以区分不同性,即:相同的对象哈希值一定相同,哈希值不同,两个对象一定不同
3.链表中的hash值
需要注意,链表中node的hash值并不是hashcode,而是hashcode经过哈希扰动之后的值,后面会讲到。
(四)链化、树化
1.链化
什么是链化?为什么要链化?
当发生hash碰撞的时候,就在数组对应的位置生成一条链表。链化是为了解决hash碰撞。
▲
哈希碰撞其他解决办法
- 开放地址法;
- 再哈希法;
- 链地址法;
- 建立公共溢出区
2.树化
当链表长度达到阈值 TREEIFY_THRESHOLD = 8,散列表length达到最小树化容量MIN_TREEIFY_CAPACITY = 64,该链表即升级为红黑树TreeNode。
3. ▲
为什么要树化使用红黑树?为什么不一开始使用红黑树?
因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。
当链表元素小于8的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于8个的时候,节点变多的复杂度就变成O(n)了,此时需要红黑树来加快查询速度;
红黑树新增节点的效率变慢了。因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。
二、 核心原理
(一)HashMap核心变量
/**
* The default initial capacity - MUST be a power of two.
* HashMap默认容量(数组长度),这个数指必须时2的n次幂
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
* table的最大容量(长度)
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
* 缺省负载因子大小。
* 这个值时经过大量数据实验得到的最佳值
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
* 树化阈值。
* 当发生hash碰撞后,会链化。当链表长度达到8时,会发生树化,链表转
* 化为红黑树
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
* 树降级成为链表的阈值
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
* 当hash表length达到这个值,并且某个链的长度达到8,才允许树化
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* The number of key-value mappings contained in this map.
* 表中的键值对个数
*/
transient int size;
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
* hash表结构修改次数
*/
transient int modCount;
/**
* The next size value at which to resize (capacity * load factor).
*
* @serial
* 扩容阈值,当hash表中的元素超过阈值时,触发扩容
* threshold = capacity * loadfactor
*/
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;
/**
* The load factor for the hash table.
*负载因子
* @serial
*/
final float loadFactor;
这几个变量尤为重要,虽然我在上面写的已经够清楚了,但是还是需要再详细说一下:
DEFAULT_INITIAL_CAPACITY
Hash表初始化默认初始容量,即table的length;注意散列表的容量capacity一定是2的n次幂!
▲
什么时候扩容?
loadFactor
:负载因子。DEFAULT_LOAD_FACTOR
:默认的负载因子threshold
:触发扩容的阈值。threshold = capacity * loadFactor 。当散列表实际使用容量达到扩容阈值,触发扩容。
▲
什么时候树化?
TREEIFY_THRESHOLD
:树化阈值,值为8;MIN_TREEIFY_CAPACITY
:最小树化容量阈值,值为64。
当实际使用容量达到64,且某一条链长度达到8,该链发生树化,单链表转化为红黑树。
▲
红黑树什么时候降级为链表?为什么
UNTREEIFY_THRESHOLD
: 红黑树降级为链表的阈值,值为6.
6和8之间存在一个差值7可以防止HashMap频繁插入、删除元素,链表元素个数在8左右变化,从而引起链表和红黑树的频繁转换,降低hashmap的效率
(二)HashMap的构造函数
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
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);
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
(三)默认容量计算
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
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的n次幂的数作为容量。
(四)put方法
/**
* 在表中将键值关联起来。
* 如果表中已经存在key对应的键值对,就替换其旧的值
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
* hash扰动
* 让key的hash值的高16位参与路由运算
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
当key位null的时候,直接将这个键值对存放到map的第一个。通过计算得到数组中将要存放元素的下标
补充一个知识点:
• 异或^ ,位运算相同为 0,不同为 1
Eg: h = 0010 0101 1010 1001 0010 0110 1100 1011
h无符号右移16位,得到其高16位的数:
0000 0000 0000 0000 0010 0101 1010 1001
^h:
0010 0101 1010 1001 0010 0110 1100 1011
=>
0010 0101 1010 1001 0000 0010 0100 0010
真正存放元素的方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//tab当前HashMap的散列表
//p:散列表中当前路由寻址下标元素
//n:当前散列表数组的length
//i:散列表中当前路由寻址的结果
Node<K,V>[] tab; Node<K,V> p; int n, i;
//将散列表数组赋值给tab
//tab==null或者数组是空的,说明散列表没有初始化,初始化数组
//作用,延迟初始化逻辑,因为创建HashMap第一时间不一定存放数据,防止浪费内存
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//- scenario 1 -最简单的一种情况:通过路由寻址,计算出所在下标。取出下标对应Node,如果为null,创建一个新的node直接存放
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//如果对应下标不为null,hash碰撞;
//e:找到的已存在的与当前要插入的key-value的key一致的元素Node。
//注意这个元素不一定存在!如果hash表中添加一个新的键值对的时候,它就不存在
//k:临时的一个key
Node<K,V> e; K k;
//比对已存在的node的key值与其hash和当前存放的key的值和其hash值时否一致
//-scenario 2- key的hash值和key值完全一致,表示已存在元素和插入键值对key-value的key一致,
//则临时取出已存在的node,后续准备替换其值value,直接执行 scenario 4
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//-scenario 3- 通过比对发现不是同一个key
//如果是红黑树,表示已经树化了,则在树中存放
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//-scenario 4- 不是红黑树,不满足 链表长度=8 && hash表size=64,则是链表
else {
//循环当前链表
for (int binCount = 0; ; ++binCount) {
//-scenario 4-1-循环到末尾未找到key相等的元素,说明链表中没有已存在key值相同的键值对node,
//就在其末尾插入新的node,跳出循环
//注意这里e=null
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果操作数大于等于 树化阈值,触发树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//树化
treeifyBin(tab, hash);
break;
}
//-scenario 4-2-找到key值和hash完全一致的node,即链表中已存在目标node,找到这个node,
//后续准备value替换,跳出循环进行 scenario 5
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//-scenario 5- 替换已经存在的键值对node的value,并返回旧的值
// 只有scenario 1 和scenario 3-2 会发现已存在的元素,然后执行这里。
//注意这里是替换操作,操作数不会变化
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//三类表结构修改次数+1,替换不计数,只有增加元素的时候才会计数
++modCount;
//增加元素之后,如果size>扩容阈值,触发扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
put方法分4种情况:
首先检查发现Node[] table 为null,说明HashMap虽然实例化,但是散列表并没有初始化,此时调扩容方法resize初始化;
- scenario 1:通过路由寻址算法找到下标(其它作者称为桶位),判断该位置元素是否存在,如果不存在,直接new一个存放;
如果不为空,这又需要看情况:- scenario 2:如果【
△
该位置的元素的hash值和正要存入的键值对的key的hash值一致并且key经过equals比较一致,或,key为同一个对象】,这说明该位置第一个(不管它是链表还是红黑树)就是我们要替换的元素对象,把这个节点元素暂存到 变量 e ,待后续修改value值;- scenario 3: 如果该节点是红黑树,则调用TreeNode.putTreeVal方法,这个方法也是检查到红黑树中已存在包含这个key的节点就返回也是暂存到变量 e,没有就创建一个并存入(
注
:这个方法属于红黑树的范畴了,写这篇帖子的时候,我还不懂红黑树,所以我不具体展开讲它的原理,可能存在误区)。- scenario 4:当前节点是链表。开始迭代链表
- scenario 4-1 : 简单情况。【
△
链表中存在某个元素的hash值和正要存入的键值对的key的hash值一致且key值一致,或者,key为同一个对象】则表示存在key一致的键值对元素,那么停止循环,元素已经暂存到e上,留到后面替换value;
注意我画了两次的重点:链表中存在某个元素的hash值和正要存入的键值对的key的hash值一致且key值一致,或者,key经过equals比较一致。则表示存在key一致的键值对元素。这就引出了一个问题:
△什么对象可以作为HashMap的key?
,或者说,△成为HashMap的key需要满足什么条件?
实体类重写hashCode方法和equals方法,使得对象的值相同时,产生的hash值一致;最好是变量私有化,不保留修改它元素的方法甚至setter,仅仅保留构造函数中的传值方式,以达到对象不变的目的。
△可不可以不重写equals?
不可以! 因为自定义实体类默认没有重写Object的equals方法,该方法是比较对象地址是否一致的。所以不重写严重后果是,命名hash值相同,key值一致对象的元素一致却无法取出hashMap中的元素,或者没有办法替换已存在键值对的值。
好,跳过这段插曲,继续讲:
- scenario 4-2:复杂情况。迭代知道链表的尾部也没有可替换的键值对元素,那么就new一个存入并存入队尾。这不是重点,重点是后面:如果当前链表长度大于等于树化阈值(注意此处阈值-1是因为迭代时候下标是从0开始的),满足条件则调树化的方法treeifyBin:
链表升级为树 treeifyBin:
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
如果散列表容量小于最小树化容量则扩容,如果大于,则将当前链表升级为树(这里我不展开讲)。可以看到,链表树化需要达到两个条件:某个链表长度达到树化阈值,散列表容量达到最小树化阈值容量
(五)resize扩容
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
//应用扩容之前的hash表
Node<K,V>[] oldTab = table;
//扩容之前的数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//扩容之前的扩容阈值(触发本次扩容的阈值)
int oldThr = threshold;
//扩容之后数组长度,// 扩容之后再次触发扩容阈值
int newCap, newThr = 0;
//已经初始化过了,是一次正常扩容
if (oldCap > 0) {
//如果之前的数组长度已经达到最大值了,则不扩容,设置扩容阈值位int最大值。这种情况很少
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,调用构造器传入了容量,然后调用tableForSize计算出了thresshold
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);
}
//如果调用构造器传入了容量,那么这个值就是0,然后一般情况是通过负载因子和容量计算得到值
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;
//如果扩容之前hash表中已经不为null了
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//当前位置由数据
if ((e = oldTab[j]) != null) {
//就数组这个位置置空
oldTab[j] = null;
//如果这个节点是单个数据,从未发生hash碰撞,非链,非红黑树。
//通过录用寻址得到的新数组下标后,直接存进去
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
// lowHead:低位的链表头部,lowTail,低位的链表尾部
Node<K,V> loHead = null, loTail = null;
// highHead:高位的链表首部,highTail:高位的链表尾部
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//通过hash值和之前的数组长度(没有-1)与运算,得到高位,要么是1,要么是0
//1-则存放到新的数组中的后半段,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);
if (loTail != null) {
//置为null,因为它的下一个很可能就是高位的链表中某一个
loTail.next = null;
//仍然存放到低位的同样位置
newTab[j] = loHead;
}
if (hiTail != null) {
//置为null,因为他们在之前的链表中,关联了下一个节点,很可能在低位的链表中
hiTail.next = null;
//存放到高位的(原来位置+原来数组长度)的位置
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
扩容方法实在太长了,上面注释也比较清楚,我就不写解析了 😂😂😂
为什么要扩容?
元素少的时候,未发生hash碰撞之前,hash表是数组结构,存放元素时间复杂度是O(1),当hash表树化以后,时间复杂度变成O(n),即每次先找到数组中对应路由地址,然后迭代链表,查找效率退化,性能变低;通过扩容缓解了 哈希冲突导致的莲花影响查询效率的问题。
(六)get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
//tab:当前hash表的散列表table
//first:当前位置的头元素
//e:临时节点
//n:当前散列表table长度
//k:临时节点的k
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//散列表不为空的情况下,通过路由寻址算法(hash &(length-1))定位到第一个元素的下标并取出
//此时这个元素可能是数组的普通元素,也可能是链,也可能是树
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// scenario 1 ,最简单情况,第一个元素 就是我们要的node
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//并非单个元素,可能链化或者树化了
if ((e = first.next) != null) {
// scenario 2 ,当前元素已经树化
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// scenario 3 ,当前元素链化
do {
//不断循环直到取出目标node
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
(七)remove
/**
* Removes the mapping for the specified key from this map if present.
*
* @param key key whose mapping is to be removed from the map
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* Implements Map.remove and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to match if matchValue, else ignored
* @param matchValue if true only remove if value is equal
* @param movable if false do not move other nodes while removing
* @return the node, or null if none
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//tab:引用当前Hash表
//p:当前node元素
//n:当前散列表长度
//index:寻址结果
Node<K,V>[] tab; Node<K,V> p; int n, index;
//取出寻址结果的元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//node:目标node
//e:循环中的当前元素
Node<K,V> node = null, e; K k; V v;
//如果第一个就是我们的目标
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
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//如果目标节点不为null,并且结果匹配(value不必要相同 或 必要相同时value一致)
//进行后面的remove操作
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);
//目标节点和之前记录的上个节点时时同一个,即在table中该位置头元素就是删除的目标
else if (node == p)
tab[index] = node.next;
//链化,把当前节点的next节点关联到上一个节点,抛弃当前节点
else
p.next = node.next;
//操作次数加1
++modCount;
//size -1
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
(八) replace 方法
@Override
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}
@Override
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;
}
没什么好说的,调用了getNode找到目标node,然后替换其值
三、 补充
这里面涉及 位运算,我并不熟悉,看了半天别人的帖子五花八门我并不能真的理解,所以没有解析;
另外提到树化,树的降级等红黑树相关,我并没有在这里展开讲,后续我会更新红黑树相关的帖子;