本文是基于JDK1.8
HashMap的底层数据结构是散列表(也叫哈希表),用拉链法来解决哈希冲突,即把冲突的节点以链表的形式添加在后面,当链表的长度大于等于8时,链表转为红黑树来提高效率(也叫数组+链表+红黑树)。
Hash桶(Bucket):是指table数组中的一个节点和链在它后面的链表或红黑树
图中用红色框框住的就是一个个桶,有的桶没有节点,有的桶的节点以链表形式存储,有的桶的节点以红黑树存储。
Table数组的大小就是桶的数量。
关键属性
// 默认的初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 桶的树化阈值:链表转为红黑树的链表长度的阈值
static final int TREEIFY_THRESHOLD = 8;
// 桶的链表还原阈值:红黑树转为链表的红黑树节点的阈值,
// 当红黑树的节点数小于UNTREEIFY_THRESHOLD 红黑树会转为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 最小树形化容量阈值:即 当哈希表中的节点 >= 该值时,才允许链表转换成红黑树
// 否则,若桶内元素太多时,则直接扩容,而不是树形化
static final int MIN_TREEIFY_CAPACITY = 64;
// table 存储Node的数组
transient Node<K,V>[] table;
// table的Set集合
transient Set<Entry<K,V>> entrySet;
// map中的entry数量
transient int size;
// map的修改次数
transient int modCount;
// threshold = 容量*加载因子 当size超过threshold时会进行扩容
// 数组扩容阈值。
int threshold;
// 加载因子
final float loadFactor;
初始化方法
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
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);
}
// 计算table的扩容阈值, 取大于等于cap的最小的2次幂
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;
}
-
new HashMap时不输入初始容量和加载因子,则使用默认的。
-
输入的话则判断initialCapacity和loadFactor 是否合理,如果合理则this.loadFactor =loadFactor。
-
tableSizeFor()这个方法的作用是找到大于等于给定容量的最小2的次幂值,赋值给 threshold。
-
在resize()方法中会把threshold设置为新容量(即table数组的长度),再令threshold = 新容量 * loadFactor;所以说构造方法输入capacity,则hashMap的容量为大于等于capacity的最小2的次幂值。
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 : >>> 表示无符号右移,右移后补0;| 或运算符:只要有一个数是1则结果为1,否则为0(位运算和二进制的左移右移操作不清楚的,可以先去了解一下)。
n |= n >>> 1;
假设输入9,n = 9-1 = 8,则8的二进制表示是:
n - - - - - - 00000000 00000000 00000000 00001000
n >>> 1:00000000 00000000 00000000 00000100
结果为:- 00000000 00000000 00000000 00001100(12)
n |= n >>> 2;
n - - - - - - 00000000 00000000 00000000 00001100
n >>> 2:00000000 00000000 00000000 00000011
结果为:- 00000000 00000000 00000000 00001111(15)
因为现在n的最高位往右都已经是1了,所以
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;的结果依然是15。
- 可以看出进行这几步位运算的目的就是将二进制数的最高位的右边全部变为1(int的二进制为32位,所以到>>>16就可以了)
然后n=n+1,二进制变为:00000000 00000000 00000000 00010000 (n=16)- 为什么最开始需要int n = cap - 1;因为这样才能取到大于等于给定容量的最小2的次幂值(如果直接拿n进行移位操作,取到的是大于给定容量的最小2的次幂值;假设输入8,则最后得到的n是16)
resize
resize方法为当Map中节点数量达到threshold 阈值时来进行扩容,或者第一次调用Put方法来初始化table数组。 final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
// 原来map的capacity
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 原来的阈值
int oldThr = threshold;
// 新的容量和阈值
int newCap, newThr = 0;
// 之前table已经存在,扩容
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 容量翻倍后小于最大容量,原来的容量>=默认初始容量
// 容量和threshold阈值 都翻倍
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
// new HashMap时指定了初始容量 HashMap(int initialCapacity)
// 构造方法中调用tableSizeFor方法 取大于等于cap的最小的2次幂, 设为容量值, tableSizeFor方法上面有做介绍
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// new HashMap时调用的是无参的构造方法
// 容量和加载因子都取默认值, 阈值 = 容量 * 加载因子
newCap = DEFAULT_INITIAL_CAPACITY;
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;
// 然后创建新的table数组,长度为容量newCap
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 如果是扩容的,则需要把之前的table中的数据移到新的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)
// 桶中只有一个节点
// 计算节点在新的table的索引index : e.hash & (newCap - 1)
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;
// 同一个桶中的节点的Hash值不一定相同
// 为了让数据更加平均,将原来桶中的元素的hashCode与oldCap &运算,为0则位置不变,否则移动到j + oldCap的位置
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;
}
// 桶中节点是以红黑树的形式存储,移动到新的table中
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this; // e
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)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) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
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);
}
}
}
上面代码都有注释,基本都能看懂,总结就是之前table不存在,则新建table数组,如果存在则两倍扩容,再将原来table的节点移到新的table中。
这里有几个点想再说明一下:
- 在计算节点在Table中的位置时为什么是 [节点的哈希值 & 容量-1]?
这样能够保证Table数组的元素均匀分布,即散列的均匀性,同时也提升了效率。
因为容量capacity是2的幂,为偶数,-1之后是奇数,这样二进制的最后一位是1,与Node.hash &
的结果取决与Node.hash,可以是奇数也可以是偶数;
而如果容量为奇数的话,很明显capacity-1为偶数,二进制的最后一位是0,这样hash&(capacity-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这样便浪费了近一半的空间。
(均匀散列的办法还有对Hash值取模,可能位运算简单并且效率高,所以JDK使用了位运算)
同一个桶中的节点的Hash值不一定相等,只是hash&(capacity-1)相等。
- 把原来table数组的节点移到新的table中为什么判断(e.hash & oldCap) == 0),等于0则位置不变,否则移动到 j + oldCap的位置?
其实也是为了让新table的数据更加均匀。因为扩容是两倍, j + oldCap 位置是扩展出来的,肯定是没有节点的。
- 如果节点是红黑树TreeNode,移到新数组的split的方法和链表移动是一样的道理,只是移动完,判断了节点数量是不是小于树退化成链表的阈值,如果小于则把红黑树转为链表。
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
- 为什么要设置TREEIFY_THRESHOLD和UNTREEIFY_THRESHOLD让链表和红黑树来回转换,而不是直接全都使用红黑树?
首先,当节点数量很多时,链表的效率会很低,用红黑树提高效率;因为红黑树的建树过程比链表复杂,而当节点的数量只有几个,没必要来建立复杂的红黑树。
put添加
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// onlyIfAbsent参数:当存入值时,如果该key已存在,是否覆盖它的value。false为覆盖,true为不覆盖 默认覆盖
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
HashMap.Node<K,V>[] tab;
HashMap.Node<K,V> p;
int n, i;
// 首次put时,table为空,调用resize()初始化数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算节点再table中的索引位置 (n - 1) & hash
// 如果为该位置没有节点,则直接添加
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
HashMap.Node<K,V> e; K k;
// 如果插入位置的首节点的key和要存入的key相同,则直接覆盖value的值
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);
else {
// 插入的桶是链表形式,
for (int binCount = 0; ; ++binCount) {
// p.next == 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;
}
// 检查链表中是否已经包含key
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 覆盖value的方法
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; // 修改次数++
// 如果节点个数大于阈值,进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
桶中的链表转为红黑树还需要table的容量(数组的长度)大于最小树化容量 MIN_TREEIFY_CAPACITY
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 桶中节点的数量 binCount >= TREEIFY_THRESHOLD - 1 , 但是table的容量(数组的长度)小于MIN_TREEIFY_CAPACITY
// 调用resize()进行扩容,不会转红黑树
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
// e: 桶中第一个节点
TreeNode<K,V> hd = null, tl = null;
// 把桶中的Node节点都next在head头节点后面
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);
// 再对head进行树化
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
具体红黑树的操作在HashMap的内部类TreeNode中,这里不介绍了,小伙伴自己去看哈,可以参考这篇红黑树的介绍文章https://blog.csdn.net/weixin_43082343/article/details/102997625
static final class TreeNode<K,V> extends 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;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
// 下面的代码省略 ......
}
get查找
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
// tab:table数组 first: 桶的头节点 n: 数组长度 k: 桶的头节点的key
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 数组不为null 数组长度大于0 桶的头节点不为null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 如果桶的头节点的hash==key的hash 或者 key和桶的头节点的key相同
if (first.hash == hash && // always check first node
((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 {
// 节点的hash==key的hash 或者 key和节点相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
remove删除
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
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<K,V> node = null, e; K k; V v;
// 如果要删除的节点刚好是key对应的桶中第一个节点,则记录下来
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 {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 删除并返回该节点
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);
// 如果找到的节点是当前桶中链表的第一个节点,则将第二个节点设为桶的头节点
else if (node == p)
tab[index] = node.next;
// 如果找到的节点不是桶中链表的第一个节点,则将找到节点的前一个节点指向其后续节点的后续节点
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
HashMap节点的定义
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; }
// Node的hashCode为key的hashCode和value的hashCode的异或 这样能尽可能的生成不同的HashCode
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的equals Node的k和v都相等 Node才相等
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;
}
}
- Node的hashCode为key的hashCode和value的hashCode的异或结果, 这样能尽可能的生成不同的HashCode
- HashMap中的Node重写了equals方法,当Node的k和v都相等时Node才相等
- 这里想再补充一点:
hashCode() 返回对象的hash码,是一个数值;不同对象的hashCode可能相同;用==判断是不是同一个对象(判断两个对象在JVM内存中的地址是不是一样)。
同一个对象的hashCode一定相同, 重写equals需要重写HashCode方法,可以减少对象的比较次数,提高效率 (HashCode不相等直接返回false)。
最后来看一下HashMap的hash方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap计算Node的Hash值,会取Node的hashCode和hashCode右移16位的结果进行异或运算的结果。
这是因为在计算节点Node在table数组中的索引位置时是这样 Node.hash & (length-1),
而我们平常使用时绝大多数情况下length都小于2^16即小于65536,所以Node.hash & (length-1)
结果始终是Node.hash的低16位与(length-1)进行&运算。 使用 (h = key.hashCode()) ^(h >>> 16) 这样可以使低16位的数字更加均匀,因为&和| 运算都会使得结果偏向0或者1。
总结
-
为什么容量Capacity要是2的幂(2^n)?
这个上面也有提到,因为计算Node节点在table数组中的位置时需要用length-1的和hash值做一个&操作,当Capacity为2的幂时,能时节点均匀分布到各个桶,减少hash碰撞的机会,提高效率。
解释下为什么会这样:
当Capacity为2的幂,比如
Capacity = 4 - - - - - 00000000 00000000 00000000 00000100
Capacity = 8 - - - - - 00000000 00000000 00000000 00001000
Capacity = 16 - - - - 00000000 00000000 00000000 00010000
Capacity = 32 - - - - 00000000 00000000 00000000 00100000
Capacity = 64 - - - - 00000000 00000000 00000000 01000000
Capacity - 1,最高位变为0,最高位的右边全变为1
Capacity-1 = 3 - - - - - 00000000 00000000 00000000 00000011
Capacity-1 = 7 - - - - - 00000000 00000000 00000000 00000111
Capacity-1 = 15 - - - - 00000000 00000000 00000000 00001111
Capacity-1 = 31 - - - - 00000000 00000000 00000000 00011111
Capacity-1 = 63 - - - - 00000000 00000000 00000000 00111111
这样hash & (length-1) 的结果只取决于hash,能均匀分布到整个数组,如果Capacity-1的二进制的最高位的右边有0,则hash & (length-1)的结果肯定是不能分配到某个桶的。
举个例子:
如果长度是17,那么length-1就是16,
16的二进制为: 00000000 00000000 00000000 00010000
则hash & (length-1)的结果只有0和16两种结果,无论添加多少节点,都只添加到这两个桶,中间的15个桶都没有用到。
-
加载因子
loadFactor加载因子表示的是Hash表中桶的填满的程度。
若加载因子越大,则填的桶越多,则空间利用率提高了,但冲突的几率增大了,链表长度或者红黑树高度越来越大,查找效率会降低。
反之,加载因子越小,则填的桶越少,冲突的几率减小,则查找的效率越高,但会浪费很多空间。
如果机器内存足够,并且想要提高查询速度则可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求则可以将加载因子设置大一点。不过一般我们都不用去设置它,让它取默认值0.75就好了。
- HashMap的put操作的时间复杂度
HashMap的put操作需要先计算hash值和桶的位置,再定位到桶(因为是数组结构,这是一次寻址,时间复杂度为O(1))。
如果桶中没有节点,则put的时间复杂度为O(1);
如果桶中节点是以链表形式存在的,则put的时间复杂度为O(1) + O(n) = O(n);
如果桶中节点是以红黑树形式存在,则put的时间复杂度为O(1) + O(logN) = O(logN)。
所以HashMap的put操作的时间复杂度可能为O(1),O(n)和O(logN)。
- Iterator
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
在使用Iterator 遍历HashMap时,是不允许通过hashMap自己来进行添加/删除操作,因为Iterator每次取下一个Node时会进行如下判断:
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
expectedModCount是在Iterator构造方法中初始化的,是HashMap开始遍历时的修改次数,如果遍历过程中,HashMap被修改了会抛出ConcurrentModificationException(这也是快速失败机制)。 遍历过程中修改HashMap需要通过Iterator来修改,如下:
Iterator iterator = hashMap.entrySet().iterator();
while (iterator.hasNext()) {
iterator.next();
iterator.remove();
}
- 最后,HashMap中很多地方都用到了位运算来使数据均匀分布,降低冲突,提高效率。希望位运算大家能好好理解下。