声明:本hashmap源码阅读主要基于小刘老师的B站源码视频,地址为:https://www.bilibili.com/video/BV1LJ411W7dP?p=8&spm_id_from=pageDriver。通过结合视频与源码的阅读形成本文档。
1. hashmap介绍
hashmap是通过存储key与value的方式存储数据的,其中Key通过hash操作而不是直接存储。文本博客首先介绍hashmap 的存储结构,介绍主要的方式的执行了流程以及源码讲解。
1.1 hashmap基本信息
先介绍hashmap的继承与实现关系,可以看到继承抽象map类,同时实现了map接口。
接口map规定了实现的map类需要完成的功能,意味着实现类在该方法上会因为存储结构和操作特点存在区别。下面为map接口的方法。这些方法也是map中常用的方法,这也体现了java的多态的特点,即我们只需要关心map有什么方法,能实现什么功能,从而屏幕不同的实现map的底层差异。
1.2 hashmap存储关系介绍
下图的map中,存在内部类entry,内部类拥有获取key、value等的方法,该类是map存储的核心数据结构。jdk中不存在map的数据结构,因此采用内部类(Entry)+数据的方式存储map数据。具体细节会在下面描述。
1.3 hashmap继承、实现关系介绍
下面为AbstractMap的方法,hashmap继承了AbstractMap,实现了基本map操作方法。当集成的方式重写了后就就会覆盖原方法。具体方法会在下面介绍。
2. hashmap存储数据结构
hashmap的存储采用散链表,即数组和链表的组合,通过链表的方式存储相同节点的元素,当量表大于8时,会变为红黑树(红黑树的特点的特点是平衡,因此可以提高查询速度)。
2.1 hashmap存储方式
hashmap存储采用散链表的方式,即通过数据+链表的方式存储元素。散链表结合了数组的查询快速以及链表的新增、删减方便的优势。图中每一个圆圈是一个存储单元,存储单元之间采用链表存储,虚线包裹的部分为数据部分,通过hash取余的方式可以快速找到对应位置。如下图将输入的key通过hash运算(算法不唯一,但具备幂等性)获取对应计算数值,通过对8取余,得到对应的存储位置,如果该位置存在多个元素,则将元素插入链表中,jdk1.8采用尾插法,1.7采用头插法,方法的优劣会在put方法分析。jdk1.8后,存储满足一定条件之后会转为红黑树,细节会在put里讨论。
2.2 hashmap存储单元
可以看出,hashmap的存储对象为Node类,该类实现了map.Entry这个内部类。查看该类的属性和构造器如下,
// hash与key都是通过final修饰,表名在一个hashmap存储中key是不可变的,hashmap存储采用散链表额方式,可以得出hash和key都是存储时需要采用的存储的方式。
final int hash; // hash值 寻找数组元素是需要用到
final K key; // 存储的key值 保证存储的唯一性。
// value 顾名思义是map单个元素的value
V value;
// 由于采用散链表 存储下一个元素的地址
Node<K,V> next;
// 注意node的构造器只有这一个,因此不能通过new Node()构造出对象后再赋值。
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
2.3 HashMap属性介绍
hashmap的属性与常量有以下几个:
// 静态final通过名字可得,这个是默认初始容量,1<<4 = 2^4 = 16。hashmap的默认容量为16.
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//通过名称得,该数据为最大容量大小,容量为2的30次方 = 1073741824
static final int MAXIMUM_CAPACITY = 1 << 30;
// 扩展系数 0.75 作为扩容阈值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 翻译过来为树化阈值,即从链表转为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
// 翻译过来为树化阈值,即小于该值从树结构变为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 最小树化容量, 数组到达64后 才可以链表转树, 未达到是只采用扩容。
static final int MIN_TREEIFY_CAPACITY = 64;
// hashmap的存储结构核心,每一条记录都是一个node
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
// 当前hashmap中的元素个数
transient int size;
// 当前hashmap结构修改次数,也可以理解为操作次数也就是删除和天极爱的次数
transient int modCount;
// 负载因子 threshold = capacity * loadFactor
int threshold;
// 扩容阈值
final float loadFactor;
小结:hashmap 的属性包含了存储结构为Node数组,存在与阈值计算的系数,树化的阈值以及反树化的阈值。同时通过树化的条件可以知道树化只有满足数组长度大于64且单个链表大于8的时候才能树化。
hashmap的构造器有4个
// 此时的来到了新建map的最后一步,生成对应的数据结构。
public HashMap(int initialCapacity, float loadFactor) {
// 安全验证,判断是传入的容量是否存在问题。
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 安全验证,当输入的容量大于最大容量时,采用采用最大容量 MAXIMUM_CAPACITY = 1073741824
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 安全验证,判断传入的loadfactor是否合法
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// 生成容量 下面会详细介绍
this.threshold = tableSizeFor(initialCapacity);
}
// 构造数据时包含一个字符型参数,该参数表示初始容量,同时采用默认loadFactor=0.75,采用this函数表示向上调用下一个构造器。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 无参构造器,会将获取默认loadFactor = 0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 输入一个现有的map 获取该map。
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 生成map 下面会详细介绍。
putMapEntries(m, false);
}
其中带有容量大小的构造器会调用tableSizeFor()
方法,该方法将数字转为大一点的最大二进制,代码如下:
static final int tableSizeFor(int cap) { // 假设传入12 , 12 = 1100B
int n = cap - 1; // n = 11
// n >>> 1无符号右移 与n取或运算(全零为0,有1为1)
n |= n >>> 1; // n >>> 1 = 110B n变为1110B
n |= n >>> 2; // n >>> 2 = 11B n变为1111B
n |= n >>> 4; // n >>> 4 = 0B
n |= n >>> 8;
n |= n >>> 16;
// 以上移动为1、2、4、8、16,通过1的数量翻倍最终使得所有的数据都是1.如12 = 1100B就变成了1111B = 15.
// 当n合法,而且不大于最大容量时,返回n+1 = 15 + 1 = 16.因此当设定容量为12是,生成的容量大小为16。
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
传入的参数为map时调用putMapEntries()
方法,该方法如下:
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 采用默认的阈值
putMapEntries(m, false);
}
// 移动元素由参数中的evict可得这个方法还有别的入口,同时该方法将参数传递给putVal。
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
// 对于初始化map来说,s > 0,并且当前table为空。则在初始化阶段会执行table == null为true的语句。
if (table == null) {
// 概述:通过原本的hashmap的size计算threshold。
/*
我们聊聊当前的复制map的现状,传入的map可能拥有任意多的数量,不一定是2的倍数,所以我们需要找到map中的阈值和容量。
阈值是容量0.75倍,因此需要计算阈值。
例如原始map的总容量为32,则阈值为24。
当输入的元素小于24时,此时阈值为24,容量为32。
当输入的元素大于24时,此事阈值为64*0.75 = 48,容量为64。
因此不能通过直接向上取2进制的方式获取最大容量。
*/
// 通过反向计算当前数量可能存在的区间,
// 比如23计算为 23 / 0.75 + 1 = 31.6 向上取2进制数为32
// 比如23计算为 24 / 0.75 + 1 = 33 向上取2进制数为64
// 比如23计算为 25 / 0.75 + 1 = 34.3 向上取2进制数为64
// 正如上面提到的 通过map中现有的数量倒推整个map中 最大值与阈值的信息。
float ft = ((float)s / loadFactor) + 1.0F;
// 安全验证不大于最大值。
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 向上取2进制数
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
// 可得出该位置是遍历移动元素,
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
// @param evict if false, the table is in creation mode.
// evict = false表名是创建模式,也就是map的初始化阶段。
putVal(hash(key), key, value, false, evict);
}
}
}
以上就是hashmap 的主要属性与构造器的解释了,我们总结一下:
-
hashmap存储的方式为node数组,通过链表的方式解决hash冲突。
-
一个hashmap必须知道正确的最大容量、扩容阈值,
-
hashmap每次扩容,数组增加一倍。上线为2的30次方。
-
hashmap的数组容量为2的倍数,扩容阈值为最大阈值的0.75倍。
3. hashmap方法介绍
在hashmap介绍中说道,实现了map接口,根据不同的存储特点与使用特点实现不同的方法,因此探索源码的主要从map中的的方法开始阅读源码。通过数据hashmap的存储结构可知,hashmap中put方法会涉及比较多的内容,同时需要了解hashmap中hash计算的方式。从上面的构造器也可知,map要时刻处理好阈值与容量的关系,因此我们需要了解扩容。
3.1 put 与 putAll
put()
方法直接调用了putVal()
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
这里用到hash
方法 我们先看看hash
是如何实现的
static final int hash(Object key) {
int h;
// (h = key.hashCode()) ^ (h >>> 16) 将高16位与低16位做异或计算,这可以充分利用数据特点。 同时不传入key时候,这之后hash值为0。
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
因此put方法是调用putVal方法
//第一个实参为hash(key)
// 第二个实参为key、
// 第三个实参为value,
// 第四个实参为false 这个形参的的名称指onlyIfAbsent,英文解释if true, don't change existing value 在putIfAbsent用到了,之指的是如果存在重复的key则不替换。
// 第五个实参为true
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// tab 值当前存储的单元
// p 为当前头元素
// n 数组长度
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 当前的数组为空 或者没有元素 需要初始化。 注意这里的写法(tab = table) == null 无论是否成功,此时的tab就指向了table。
if ((tab = table) == null || (n = tab.length) == 0)
// 此时表名 map里面的table没有初始化,也就table里面啥都没有 -> 这里是延迟初始化
n = (tab = resize()).length;
// (n - 1) & hash 这个是干掉高位 也就是对数组取模。
// 此时的p为数组的头元素。
if ((p = tab[i = (n - 1) & hash]) == null)
// 头元素为空,证明此时的链表还没有创建。把当前元素封装成为node就可以。
tab[i] = newNode(hash, key, value, null);
else {
// 此时有数据 可能是链表也可能是红黑树。
// 声明节点元素
Node<K,V> e; K k;
// p上面获取的头元素,确认当前的hash与当前元素的hash是同一个值,判断key是否是相同,如果都相同标识头结点就是需要修改的位置,即找到了需要更新的位置。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
// 判断是红黑树加入树节点,
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 判断结果为链表 遍历链表 找到想要是否存在相同的key值,
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {// 遍历到最后一个节点了 没有相同的key则新建。并插入
p.next = newNode(hash, key, value, null);
// 每次Bincount自增1 大于8-1 = 7 时候树化,记住这里是从0开始计算的,进入if如果成立表名之前的循环执行了8次,所以是插入前是8个元素了,插入之后是9个。注意如果是第一次树化,前面有8个元素,加上刚才插入的应该是9个元素树化(注意bincount的增加时机,是循环结束时候) (说实话 本人这对这个9个元素再树化不太理解,二叉树的平衡结构是7个元素,那为什么不在恰好排满3层时候直接树化)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 树化
treeifyBin(tab, hash);
// 找到最后一位而且不足8个。
break;
}
// 找到元素
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 经过循环的结果的e有可能是最后一个元素也可能是目标值。
if (e != null) { // 如果成立说明之前存在key相同的元素
V oldValue = e.value;
// 判断是否覆盖,这里的onlyIfAbsent本身用于甄别是否需要替换元素
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 操作数加一
++modCount;
if (++size > threshold) // 判断自增后的扩容阈值 判断是否扩容
resize();
afterNodeInsertion(evict);
return null;
}
3.2 resize
resize是hashmap 的扩容方法。当hashmap中的元素过多时候会导致链表过长,从而影响查询效率,因此需要改变链表的长度,除了树化就是扩大数组的长度,通过增加数组长度从而减少单个链表的长度。
resize的作用,通过增加数组长度,从而分流链表的元素。
final Node<K,V>[] resize() {
// oldTab 扩容前的表(数组)
Node<K,V>[] oldTab = table;
// 获取扩容前的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 获取扩容阈值
int oldThr = threshold;
// 新的容量,新的阈值 注意 这里无论是长度还是容量都不能为负数。
// 容量在什么时候更新的? 初始容量可以通过构造器输入,采用无参构造器可以跳过复制过程。也就是没有容量意味着map还没有初始化。
// 阈值什么时候初始化的? 与容量初始化位置一致,通过方法 “this.threshold = tableSizeFor(initialCapacity);”获取初值。
int newCap, newThr = 0;
// oldCap 容量大于0,也就是有容量,不为空。
if (oldCap > 0) {
// 散列表初始化过了 说明本次的扩容是基操,也就是传统的库容
if (oldCap >= MAXIMUM_CAPACITY) {// 如果大于定义的说明数组不能再这个增加了,标识这个map不能再resize()
// 设置下次扩容阈值为无穷大
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 可以扩容, <<1 说明增加了一倍。 同时需要保证 1.扩容后的容量小于最大容量 2.扩容前的容量大于初始值
// oldCap >= DEFAULT_INITIAL_CAPACITY 这里只的是正常初始化的要求。在制定map大小的方式时可能产生不成立的情况。
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 阈值扩大一倍
newThr = oldThr << 1; // double threshold
}
// oldCap == 0 标识map还没有初始化
else if (oldThr > 0) // initial capacity was placed in threshold
// 这种情况什么时候会产生? 通过调用第四个构造器(hashMap(Map map)调用这个构造器) 这时候只计算了阈值,没有计算容量。
newCap = oldThr;
// oldThr == 0 && oldCap == 0
else { // zero initial threshold signifies using defaults
// 情况: 调用new hashmap 没有穿任何参数情况。
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// newThr为表名通过new产生table,同时以上通过分支“else if (oldThr > 0)” 执行完毕。 说明这里的是
//1.通过new hashmap(Map map)的方式产生了table,同时目前还没有添加元素就进入了扩容。虽然之前计算过传入map的threshold值,但是扩容后需要重新计算。
//2.通过“if (oldCap > 0) {”成立进入,并且由“newCap = oldCap << 1”已经将容量扩大了一倍,(注意:“if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&” 这句话中无论结果是true还是false都会只执行“newCap = oldCap << 1”,因此此时获取的newCap已经是扩大了的)
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
// 计算一个合理的阈值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// ----------------------------以上计算了扩容后的参数(阈值和容量)---------------------------------
// ----------------------------下面开始正式扩容---------------------------------
/*
table里面的数据有链表和红黑树,所以数据转移就存在这三种情况 链表->链表; 红黑树->红黑树; 红黑树->链表
*/
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 判断就table如果为null 表示这时候是初始化。
if (oldTab != null) {// 表名以前有元素需要进行迁移
for (int j = 0; j < oldCap; ++j) {// 遍历旧的表
Node<K,V> e;
// 获取table每个位置的元素,准备对元素开始迁移。
if ((e = oldTab[j]) != null) {// 获取到了数组的头结点
//方便JVM回收
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
/*
举例: 旧表元素为有16个扩大到32,比如取11 = 1011B,可以确定最后4位。但是倒数第五位并不知道,如果是为0则扩容后还是11,否则为11+16=27,也就是说11位置的链表扩容后只能到11位置和27位置。
*/
// 两个两边的头结点与尾结点
Node<K,V> loHead = null, loTail = null; // 低位链表 例子中的11
Node<K,V> hiHead = null, hiTail = null; // 高位链表 例子中的27
Node<K,V> next;
// 遍历链表
do {
next = e.next;
// oldCap为容量对应的2进制就是新扩容的位置的 16容量的table 下表范围为0000-1111 (0-15),16位置正好是10000,即通过上面的我们得到的迁移后的区别正好是与久容量相与的结果。
if ((e.hash & oldCap) == 0) {
// 例子中的11
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
例子中的27
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 调整两个表的最后一个元素,并挂到table中。
if (loTail != null) {
// 这里避免旧的链表next不清理导致存储错误。
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
3.3 get
查询分两步,第一步找到table头结点;第二步找到链表或者红黑树里面的节点。
public V get(Object key) {
Node<K,V> e;
// 通过getNode获取目标节点获取目标node,如果有就返回没有就返回null
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
确定一个元素是要的元素,需要对比hash,计算hash与传入的hash一致,同时key也要相同。getNode如下:
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 确定table初始化过 同时获取了(n - 1) & hash位置的头元素(first)
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// always check first 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) {
// 后面有内容 需要判断是链表还是红黑树
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;
}
3.4 remove
相较于get,remove结果需要判断是否反树化。
// 与get一致
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
其中removeNode
方法为:
// matchValue 为true时,key和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;
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);
}
}
// -------------------------上面与getNode方法相似 找到要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);
else if (node == p)
// 第一个链表节点
tab[index] = node.next;
else
// 其他的节点
p.next = node.next;
++modCount; // 计数操作
--size; // 计数减一
afterNodeRemoval(node);
return node;
}
}
return null;
}
3.5 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;
}
// ---------------------afterNodeAccess----------------------------------------
// 这是对应的函数 是一个补充方法,官方解释为 LinkHashmap用于操作后的操作,类似于一个切面。
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
// -------------------------------------------------------------
// 采用与 forEach类似的操作,将核心的部分以function的方式暴露出去。
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Node<K,V>[] tab;
if (function == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
e.value = function.apply(e.key, e.value);
}
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
3.6 forEach
这里在提一下forEach,这里采用了函数式编程的方式,我们回想一下使用forEach遍历的方式
map.forEach((k,v)->{
print("key:" + k + "value:" + v);
})
传入的函数是lamdba表达式,这里接收用BiConsumer(Consumer属于一个消费者函数即没有输出形的函数)接收。
public void forEach(BiConsumer<? super K, ? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
// 遍历了table中的所有元素
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
// 执行lamdba表达式
action.accept(e.key, e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}