HashMap 是Java日常开发常用的一个集合类。Map集合即Key-Value的集合,前面加个Hash,即散列,无序的。所以HashMap是一个用于存储Key-Value键值对的无序集合,每一个键值对也叫做Entry。
这里我们先做回答,解决几个面试常问的HashMap问题,借此方式来初步了解HashMap的特性。
HashMap的底层是怎么实现的?&& HashMap的扩容机制?
在JDK1.8之前,HashMap采用数组+链表实现,即使用拉链法解决hash冲突。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值查找要遍历链表,时间复杂度为O(N),效率较低。因此JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),搜索的时间复杂度为O(logN),这样大大减少了查找时间。
为什么说HashMap是线程不安全的?
HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。
HashMap的长度为什么是2的幂次方?
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。
其他的特性?
这里我们主要的阅读材料是JDK1.8的HashMap源码
在网上找到一张很好的图
可以说HashMap就是一个桶数组,当桶内数据超过八个之后,桶内存储结构会由链表变成红黑树。
大体结构我们了解了,接下来是具体编码了
类的属性:
public class HashMap extends AbstractMap implements Map, Cloneable, Serializable {
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node[] table;
// 存放具体元素的集
transient Set> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值(容量*填充因子) 当实际大小超过临界值时,会进行扩容
int threshold;
// 加载因子
final float loadFactor;
}
复制代码
HashMap内部链表节点类的实现:
static class Node implements Map.Entry {
final int hash;
final K key;
V value;
Node next;
Node(int hash, K key, V value, Node 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;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
复制代码
HashMap内部红黑树节点类型的实现
static final class TreeNode extends LinkedHashMap.Entry {
TreeNode parent; // red-black tree links
TreeNode left;
TreeNode right;
TreeNode prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node next) {
super(hash, key, val, next);
}
/**
* Returns root of tree containing this node.
*/
final TreeNode root() {
for (TreeNode r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
/**
* Ensures that the given root is the first node of its bin.
*/
static void moveRootToFront(Node[] tab, TreeNode root) {
}
/**
* Finds the node starting at root p with the given hash and key.
* The kc argument caches comparableClassFor(key) upon first use
* comparing keys.
*/
final TreeNode find(int h, Object k, Class<?> kc) {
}
/**
* Calls find for root node.
*/
final TreeNode getTreeNode(int h, Object k) {
}
/**
* Tie-breaking utility for ordering insertions when equal
* hashCodes and non-comparable. We don't require a total
* order, just a consistent insertion rule to maintain
* equivalence across rebalancings. Tie-breaking further than
* necessary simplifies testing a bit.
*/
static int tieBreakOrder(Object a, Object b) {
}
/**
* Forms tree of the nodes linked from this node.
*/
final void treeify(Node[] tab) {
}
/**
* Returns a list of non-TreeNodes replacing those linked from
* this node.
*/
final Node untreeify(HashMap map) {
}
/**
* Tree version of putVal.
*/
final TreeNode putTreeVal(HashMap map, Node[] tab,
int h, K k, V v) {
}
/**
* Removes the given node, that must be present before this call.
* This is messier than typical red-black deletion code because we
* cannot swap the contents of an interior node with a leaf
* successor that is pinned by "next" pointers that are accessible
* independently during traversal. So instead we swap the tree
* linkages. If the current tree appears to have too few nodes,
* the bin is converted back to a plain bin. (The test triggers
* somewhere between 2 and 6 nodes, depending on tree structure).
*/
final void removeTreeNode(HashMap map, Node[] tab,
boolean movable) {
}
/**
* Splits nodes in a tree bin into lower and upper tree bins,
* or untreeifies if now too small. Called only from resize;
* see above discussion about split bits and indices.
*
* @param map the map
* @param tab the table for recording bin heads
* @param index the index of the table being split
* @param bit the bit of hash to split on
*/
final void split(HashMap map, Node[] tab, int index, int bit) {
}
/* ------------------------------------------------------------ */
// Red-black tree methods, all adapted from CLR
static TreeNode rotateLeft(TreeNode root,
TreeNode p) {
}
static TreeNode rotateRight(TreeNode root,
TreeNode p) {
}
static TreeNode balanceInsertion(TreeNode root,
TreeNode x) {
}
static TreeNode balanceDeletion(TreeNode root,
TreeNode x) {
}
/**
* Recursive invariant check
*/
static boolean checkInvariants(TreeNode t) {
}
}
复制代码
一般情况下我们都会使用无参构造 HashMap。但当我们对时间和空间复杂读有要求的时候,我们需要手动调参,以让 HashMap 满足我们的要求。可供我们调整的参数有两个,一个是初始容量 initialCapacity,另一个负载因子 loadFactor。设定这两个参数会影响到阈值的大小。初始阈值 threshold 由 initialCapacity 经过移位操作计算得出。他们的作用分别如下:
名称 | 用途 |
initialCapacity | HashMap 初始容量 |
loadFactor | 负载因子 |
threshold | 阈值(当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容) |
相关代码如下:
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The next size value at which to resize (capacity * load factor).
*
* @serial
*/
int threshold;
/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;
复制代码
我们首先从阈值(threshold)开始分析
通过看代码我们知道了HashMap初始容量是16,负载因子是0.75,而阈值我们从注释中知道是由容量乘以负载因子计算得来的。
本着求实的态度,我们查看下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);
}
复制代码
tableSizeFor方法:
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;
}
复制代码
看到这个tableSizeFor方法,似乎与上面说的 threshold=capacity * load factor 有出入。
这个算法的作用就是找到大于或等于 cap 的最小2的幂。过程就是将二进制数从左往右数的第一个1开始右边全部变成1,最后再加上1,就是结果了。
比如我们传入的cap为31,观察n的变化过程:
1 1110(30) -> 1 1111 -> 10 0000(31 + 1) -> 32
事实也是如此:
以上就是初始阈值(threshold)的计算过程了。接着我们看看负载因子(loadFactor)。
先问几个问题:
所以负载因子实际是反应了 HashMap 桶数组的使用情况:
正常来说默认的0.75就已经是很合理的解了,如果有特殊需求,可以按照上面讲的进行调整。
HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过(n - 1) & hash判断当前元素存放的位置(这里的n指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
jdk1.8 的 hash 方法
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
复制代码
对比 jdk1.7 的hash方法
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
复制代码
1.7的hash算法扰动了4次,所以相比于1.8的算法是比较慢的。
查找的入口方法在get, 主要逻辑在getNode方法
查找操作,先定位键值对所在的桶位置,然后再对链表或者红黑树进行查找。
public V get(Object key) {
Node e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
// 1. 定位键值对所在桶的位置
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) {
// 2. 如果first是TreeNode类型,则使用红黑树查找方法
if (first instanceof TreeNode)
return ((TreeNode)first).getTreeNode(hash, key);
// 2. 否则对链表进行查找
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
复制代码
查找核心在于getNode()方法。里面已经做了注释。
我们先看到第一步“定位键值对所在桶的位置”,实现代码如下:
first = tab[(n - 1) & hash
复制代码
这里通过(n-1)&hash可以算出桶在桶数组中的位置。解释一下,HashMap中桶数组的大小 length 总是 2 的幂。此时,(n-1)&hash等价于对length取余。但取余效率没有位运算高,所以(n-1)&hash也是一个小的优化。
假设 hash=143,n=16。计算过程如下:
最终结果为15,刚好就是143%16的结果。
对于插入操作我们先分析大体流程:
首先肯定是先定位要插入的键值对属于哪个桶,定位到桶后,再判断桶是否为空。如果为空,则将键值对存入即可。如果不为空,则需将键值对接在链表最后一个位置,或者更新键值对。
当然插入过程还有很多细节。首先HashMap是变长集合,需要考虑扩容问题。其次,在JDK1.8中,HashMap引入了红黑树优化过长链表,所以这里要考虑优化过程。
插入操作的入口在put(K,V),核心在V putVal(int, K, V, boolean, boolean)方法中。
首先我们看下插入操作的源码:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
// 初始化桶数组 table,table 被延迟到插入新数据时再进行初始化
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 {
Node e; K k;
// 如果键的值以及节点 hash 等于链表中的第一个键值对节点时,则将 e 指向该键值对
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果桶中的引用类型为 TreeNode,则调用红黑树的插入方法
else if (p instanceof TreeNode)
e = ((TreeNode)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;
}
// 条件为 true,表示当前链表包含要插入的键值对,终止遍历
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 判断要插入的键值对是否存在 HashMap 中
if (e != null) { // existing mapping for key
V oldValue = e.value;
// onlyIfAbsent 表示是否仅在 oldValue 为 null 的情况下更新键值对的值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 键值对数量超过阈值时,则进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
复制代码
putVal的主要逻辑:
可以看出来resize(扩容)在插入操作的逻辑中非常重要,接下来我们就讲HashMap的扩容机制。
背景知识:在 HashMap 中,桶数组的长度均是2的幂,阈值大小为桶数组长度与负载因子的乘积。当 HashMap 中的键值对数量超过阈值时,进行扩容。
需要注意:进行扩容,会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,是非常耗时的。在编写程序中,要尽量避免 resize。
HashMap按照当前桶数组长度的2倍进行扩容,扩容之后,要重新计算键值对的位置,并把它们移动到合适的位置上去。
具体实现:
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 如果 table 不为空,表明已经初始化过了
if (oldCap > 0) {
// 当 table 容量超过容量最大值,则不再扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 按旧容量和阈值的2倍计算新容量和阈值的大小
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
/*
* 初始化时,将 threshold 的值赋值给 newCap,
* HashMap 使用 threshold 变量暂时保存 initialCapacity 参数的值
*/
newCap = oldThr;
else { // zero initial threshold signifies using defaults
/*
* 调用无参构造方法时,桶数组容量为默认容量,
* 阈值为默认容量与默认负载因子乘积
*/
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// newThr 为 0 时,按阈值计算公式进行计算
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 创建新的桶数组,桶数组的初始化也是在这里完成的
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 如果旧的桶数组不为空,则遍历桶数组,并将键值对映射到新的桶数组中
for (int j = 0; j < oldCap; ++j) {
Node 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)e).split(this, newTab, j, oldCap);
else { // preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node 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;
}
复制代码
resize方法的主要逻辑:
以上我们就讲完了扩容的主体逻辑。扩容后,红黑树节点和普通节点一样是需要重新映射的。于是我们接着就说红黑树的分组和重新映射。(split方法的调用可以在resize方法体中找到。)
// 红黑树转链表阈值
static final int UNTREEIFY_THRESHOLD = 6;
final void split(HashMap map, Node[] tab, int index, int bit) {
TreeNode b = this;
// Relink into lo and hi lists, preserving order
TreeNode loHead = null, loTail = null;
TreeNode hiHead = null, hiTail = null;
int lc = 0, hc = 0;
/*
* 红黑树节点仍然保留了 next 引用,故仍可以按链表方式遍历红黑树。
* 下面的循环是对红黑树节点进行分组,与上面类似
*/
for (TreeNode e = b, next; e != null; e = next) {
next = (TreeNode)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
// 如果 loHead 不为空,且链表长度小于等于 6,则将红黑树转成链表
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
/*
* hiHead == null 时,表明扩容后,
* 所有节点仍在原位置,树结构不变,无需重新树化
*/
if (hiHead != null)
loHead.treeify(tab);
}
}
// 与上面类似
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
复制代码
重新映射红黑树的逻辑和重新映射链表的逻辑基本一致。不同的地方在于,重新映射后,会将红黑树拆分成两条由 TreeNode 组成的链表。如果链表长度小于 UNTREEIFY_THRESHOLD,则将链表转换成普通链表。否则根据条件重新将 TreeNode 链表树化。
举个例子:
我们对该桶数组进行扩容,扩容后需要重新映射上面的红黑树,映射结果如下:
JDK1.8对HashMap实现进行了改进。最大的改进就是引入了红黑树。
关于红黑树可以参考我的另一篇博客:juejin.cn/post/713683…
树化相关的代码:
static final int TREEIFY_THRESHOLD = 8;
/**
* 当桶数组容量小于该值时,优先进行扩容,而不是树化
*/
static final int MIN_TREEIFY_CAPACITY = 64;
static final class TreeNode extends LinkedHashMap.Entry {
TreeNode parent; // red-black tree links
TreeNode left;
TreeNode right;
TreeNode prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node next) {
super(hash, key, val, next);
}
}
/**
* 将普通节点链表转换成树形节点链表
*/
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e;
// 桶数组容量小于 MIN_TREEIFY_CAPACITY,优先进行扩容而不是树化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
// hd 为头节点(head),tl 为尾节点(tail)
TreeNode hd = null, tl = null;
do {
// 将普通节点替换成树形节点
TreeNode 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);
}
}
TreeNode replacementTreeNode(Node p, Node next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
复制代码
扩容的过程中,树化需要满足两个条件:
HashMap在设计之初,没有考虑到以后会引入红黑树进行优化。所以并没有像TreeMap一样,要求键类实现Comparable接口,或提供对应的比较器。但由于树化的过程需要比较两个键对象的大小,在键类没有实现Comparable接口的情况下,这成了一个问题。HashMap做了三步操作。通过下面三步操作就能知道谁大谁小,最终构造出红黑树了。
红黑树中仍然保留了原链表节点的顺序。有了这个前提,将红黑树转化成链表,仅需将 TreeNode 链表转换成 Node 类型的链表即可。
final Node untreeify(HashMap map) {
Node hd = null, tl = null;
// 遍历 TreeNode 链表,并用 Node 替换
for (Node q = this; q != null; q = q.next) {
// 替换节点类型
Node p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
Node replacementNode(Node p, Node next) {
return new Node<>(p.hash, p.key, p.value, next);
}
复制代码
HashMap 的删除操作并不复杂,入口方法为remove,主要逻辑在removeNode方法。
删除仅需三个步骤即可完成。第一步是定位桶位置,第二步遍历链表并找到键值相等的节点,第三步删除节点。相关源码如下:
public V remove(Object key) {
Node e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node[] tab; Node p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
// 1. 定位桶位置
(p = tab[index = (n - 1) & hash]) != null) {
Node 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) {
// 如果是 TreeNode 类型,调用红黑树的查找逻辑定位待删除节点
if (p instanceof TreeNode)
node = ((TreeNode)p).getTreeNode(hash, key);
else {
// 2. 遍历链表,找到待删除节点
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 3. 删除节点,并修复链表或红黑树
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
复制代码
总共的遍历方式在查阅后发现有7种。
遍历方式主要分成下面的四个方向:
entrySet的性能比keySet的性能高,所以我们还是尽量使用entrySet来遍历HashMap。
HashMap是Java学习者绕不过去的一道坎,无论是应付面试或者是实际的应用都要经常和这个数据结构打交道。本文参考了很多前人的分析并由自己消化,最终呈现出来。不得不说对源码的深入理解需要大量的功夫,需要融汇诸多思考。所以在我有新的思考之后可能会对文章再做更新,希望能为这思考再加上一层。
作者:pixel_revolve
链接:https://juejin.cn/post/7142388342103998494
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
留言与评论(共有 0 条评论) “” |