1. Map
Map是一种存储键值对(key-value)的容器,容器中一个key映射到一个唯一的value,所有的key不可以重复。
Map提供了三种集合视图,分别是key的集合(不重复)、value的集合(可重复)和key-value对的集合(entry,不重复)。
一个Map实例自身不可以作为自己的key,但是可以作为自己的value。
Map家族使用较多的实现类是HashMap、LinkedHashMap。本文将主要介绍这两个类的实现方式 (jdk11版本)。
2. HashMap
HashMap是Map接口的一种实现类,其允许key为null,也允许value为null。
HashMap很大程度上类似于一个古老的Map实现类Hashtable,区别在于HashMap不是线程安全的,并且允许null的存储;而Hashtable则是线程安全的,不允许存储null。因此,HashMap的操作效率高于Hashtable。如果需要在多线程环境下使用HashMap,应该做好外部的同步操作。
HashMap不保证其存储的元素的顺序。
HashMap有两个重要的参数会影响其性能:初始容量和装载因子。
初始容量,即HashMap被创建时,表中槽(bucket)的个数。
装载因子,即hash表被填满的程度。装载因子越大,说明hash表中的空闲位置越少。装载因子默认是0.75,值越大,空间利用越高,但是查找效率越低。
如果节点的个数✖️装载因子的值,超过了hash表的容量,就会进行做rehash操作,加大了开销。因此,当我们在设置hash表容量的时候,应该综合考虑键值对的个数和装载因子。
另一个值得注意的情况是,不应该出现太多相同的键(key)。当键相同时,这些相同键的节点,将以链表或者红黑树的数据结构进行存储,查询效率低于hash方式。
2.1 类属性
①默认的初始容量为16,必须是2的整数次幂。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
②最大容量为2的30次幂。
static final int MAXIMUM_CAPACITY = 1 << 30;
③默认的装载因子是0.75。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
④树化的阈值是8。jdk8以前,单个槽上的所有节点都以链表的形式存储。jdk8开始,当某个槽上装载的节点个数等于8,如果此时在这个位置再添加一个节点,将可能进行树化,改为使用红黑树存储。所以,树化的一个条件是:单个槽装载的节点个数大于8。
static final int TREEIFY_THRESHOLD = 8;
⑤在重新调整hash表的大小时,如果某个槽上装载的节点个数小于TREEIFY_THRESHOLD,并且不超过UNTREEIFY_THRESHOLD,会进行去树化。
static final int UNTREEIFY_THRESHOLD = 6;
⑥树化的最小容量值。如果某个槽的节点个数过多,但hash表中的节点总数小于MIN_TREEIFY_CAPACITY,将进行扩容。如果节点总数超过了该值,将进行树化。所以,树化的第二个条件是:hash表的节点总数大于等于64。
static final int MIN_TREEIFY_CAPACITY = 64;
⑦链表形式的节点是以Node类来实现( jdk8开始 )。Node类实现了Entry接口。
static class Node<K,V> implements Map.Entry<K,V> {
//节点的哈希值,不会发生改变,所以用final修饰
final int hash;
//键,不会发生改变,所以用final修饰
final K key;
//值,会发生改变
V value;
//由于是以链表形式相连,所以需要next指针
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; }
//以key=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;
//如果是Entry类型的话
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
//如果key和value都相等的话,就是相同的
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
⑧红黑树形式的节点是以TreeNode类来实现的。红黑树作为一种比较复杂的数据结构,我们需要了解其应用的场景,然后可以熟练地调用。红黑树适用于动态插入、删除和查找的场景,其查找的时间复杂度为O(logN)。
static final class TreeNode<K,V> extends LinkedHashMap.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);
}
2.2 实例属性
①Node数组作为hash表,存储键值对
transient Node<K,V>[] table;
②所有键值对的集合
transient Set<Map.Entry<K,V>> entrySet;
③容器中的键值对总数
transient int size;
④容器被修改的次数
transient int modCount;
⑤扩容的阈值。每次添加完一个节点,都要判断总节点数(size)是否大于threshold,如果大于,则进行扩容。
int threshold;
⑥hash表的装载因子,由final修饰,一旦被赋值,不可再修改。
final float loadFactor;
2.3 构造器
①空参构造器。使用了默认的装载因子0.7。所有其他的属性都采用默认值。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
②使用给定初始容量和装载因子的构造器。
public HashMap(int initialCapacity, float loadFactor) {
//传入的初始容量必须大于等于0
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方法将返回大于等于容量cap的最小的2的整数次幂。比如,当我们传入10,将得到16;当我们传入8,将得到8。
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
那么,这个方法是如何实现的?
以cap=10为例进行说明:
首先计算10-1 = 9 (十进制) = 1001 (二进制);
再计算1001的高位的0的个数,共28个;
然后,将-1(也就是二进制的32个1)逻辑右移28位,得到1111,于是n=1111 (十进制15);
最后结果为n+1=16。(注:逻辑右移时,高位全部填0。)
简而言之,就是cap-1有几位,就让n等于几个1,然后再加1,就可以取得大于等于cap的最小2的整数次幂。
至于为什么要将cap-1,是因为当cap正好等于2的整数次幂时,应该返回这个值自身。比如,cap=8,tableForSize(8)应该返回8,如果不减1,就会得到16。
并且实际上,这个threshold将会是第一次创建的hash表的长度。
③使用给定初始容量的构造器。该方法使用了默认的装载因子0.75。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
值得注意的点是,HashMap也使用了延时分配空间的策略,在构造实例时,并不创建用于存储键值对的hash表。
2.4 添加一个节点
put(K, V)方法向容器中添加键值对。
public V put(K key, V value) {
//首先计算key的hash值,然后调用putVal方法,代码见下文
return putVal(hash(key), key, value, false, true);
}
putVal方法。如果是刚创建了实例,第一次放入元素时,会调用扩容方法resize()来新建一个hash表。
然后通过哈希函数(n-1) & hash,确定新的元素放入哪个位置。
n为表的长度,由于它被设定为2的整数次幂,那么n-1的二进制数必定全部都是1,且n-1为数组的最后一位。
实际上就是取n-1的二进制位数(假设为m位),然后取hash值的最后m位的值,作为要添加的位置,这个值必定小于等于n-1。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果还没有创建hash表,则调用resize方法,创建一张表,代码见下文。
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<K,V> e; K k;
//p为当前位置上的第一个节点,比较节点p的hash值是否相同,再比较是否为同一个key或者key的值相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//如果要添加的key和已存在的key相同,用e来保存旧的节点
e = p;
//如果p的key和要插入的key不同,则继续比较下一个节点。
//现在又分两种情况:①当前位置上存放的是红黑树
else if (p instanceof TreeNode)
//则将新的节点插入到红黑树中
e = ((TreeNode<K,V>)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);
//如果当前位置上存放的元素数大于树化的阈值8(binCount是从0开始的)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//尝试将单链表改为红黑树存储,代码见下文
treeifyBin(tab, hash);
break;
}
//判断当前遍历的节点的hash值和key是否与要插入的节点相同
//判断顺序:(使用以下顺序,可以最快地判断这个key是否是我们要找的key)
//①hash值是否相同,不同则可以直接认为不是同一个key
//②hash值相同的情况下,判断key的地址是否相同,如果相同,则肯定是同一个key,不做后续判断
//③如果key的地址不同,则继续使用equals方法来判断
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//p始终指向前一个节点,以保证链表可以插入
p = e;
}
}
//如果当前位置存在节点与要插入的节点的key相同,那么e就指向那个节点
if (e != null) {
//保存该节点的值
V oldValue = e.value;
//该节点处,用新值替换旧值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//返回旧值
return oldValue;
}
}
++modCount;
//每次添加完元素,需要判断是否超过了threshold,如果超过,则进行扩容
if (++size > threshold)
resize();
//如果是LinkedHashMap调用put方法时,这里需要做特殊的处理
//如果是普通的HashMap,这里不做任何处理
afterNodeInsertion(evict);
//如果没有修改旧值,则返回null
return null;
}
treeifyBin方法尝试将hash表中某一上的所有节点改为使用红黑树存储。当表的长度大于等于64时,才会进行树化;否则执行扩容操作。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果当前hash表的长度小于可以树化的最小表长度64,则不进行树化,而是扩容
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方法。在扩容时,新容量设为旧容量的两倍。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//第一次添加元素时,没有新建表,oldCap为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//获得初始时设置的threshold
int oldThr = threshold;
int newCap, newThr = 0;
//如果是扩容的情况
if (oldCap > 0) {
//如果旧的capacity已经超出了最大容量,则将threshold设为int的最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
//表不作扩容
return oldTab;
}
//否则,新的容量设为旧容量的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//当旧的容量大于等于初始默认容量时,新的threshold设为旧threshold的2倍
newThr = oldThr << 1; // 否则,按照新的capacity乘以装载因子,来计算新的threshold
}
else if (oldThr > 0) // 使用之前设置的threshold作为新的capacity
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//第一次添加元素时或者oldCap小于初始默认容量时,newThr为0
if (newThr == 0) {
//计算新的threshold,为容量capacity乘以装载因子
float ft = (float)newCap * loadFactor;
//如果新的capacity大于最大容量,或者计算出的ft大于最大容量,则取int的最大值作为新的threshold
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//新建一张hash表,长度为之前的threshold
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//如果是扩容的情况,需要将旧表的元素迁移到新表
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)
//重新计算在新表中的位置,然后直接放入
newTab[e.hash & (newCap - 1)] = e;
//如果是红黑树节点,做对应的操作
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果是链表节点
else { // 将该链表上的节点分为两类:
// 第一类节点的hash&oldCap==0,第二类节点的hash&OldCap!=0
// 第一类节点在扩容一倍的hash表中还是存放在原处
// 而第二类节点在新hash表中将存放在当前位置向后偏移oldCap的位置(newCap等于2倍的OldCap)
//这种方式只是简化了重新计算在新表的存放位置,因为它等价于将节点的hash&(newCap-1)取得的新位置
//第一类节点组成的链表,将用loHead保存头指针,loTail保存尾指针
Node<K,V> loHead = null, loTail = null;
//第二类节点组成的链表,将用hiHead保存头指针,hiTail保存尾指针
Node<K,V> hiHead = null, hiTail = null;
//next用于保存原链表上的下一个节点
Node<K,V> 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);//直到下一个节点为null
//将第一类链表接到新表的j位置,j也是其在旧表中的位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//将第二类链表接到新表的j+oldCap位置
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//返回新建的hash表
return newTab;
}
注意:
java8以前,将发生冲突的节点以链表形式连接,执行put方法时,以头插法的形式,将新节点插入到链表头部。在多线程环境下,使用这种方式来put元素,可能会导致链表成环。
java8开始,将发生冲突的节点以链表或者红黑树连接。当发生冲突的节点较少时,以尾插法将节点插入链表;当发生冲突的节点较多时,使用红黑树存储,加快查询速度。
2.5 添加一个map
putAll方法向本HashMap中添加给定的map中的所有键值对。
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//s等于要添加的map的元素数
int s = m.size();
if (s > 0) {
//如果当前map是空的
if (table == null) {
//计算新表的阈值
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//如果计算出来的阈值大于初始阈值
if (t > threshold)
//取大于等于t的最小的2的整数次幂作为threshold,后续该值将作为新表的capacity
threshold = tableSizeFor(t);
}
//如果要添加的元素数超过了本map的阈值,就算本map为空也放不下,所以要先扩容
else if (s > threshold)
resize();
//调用entrySet方法,取得要添加的所有键值对组成的集合,然后一个一个依次添加到本map中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
//在一个一个添加的过程中,也会涉及到扩容
putVal(hash(key), key, value, false, evict);
}
}
}
2.6 删除一个节点
①remove(key)方法给定一个key,删除相应的键值对。删除之前,需要查找到这个节点。如果找不到,则返回null;否则,返回改节点的value。
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) {//matchValue表明是否value也有匹配
Node<K,V>[] tab; Node<K,V> p; int n, index;
//hash表不空时才可能删除,取得要删除节点的索引位置的第一个节点,p指向这个节点
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;
//如果第一个节点就是要删除的节点,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 {//如果是单链表,依次遍历每个节点,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);
}
}
//如果找到了要删除的节点,然后看要不要连value也匹配,remove(key)不匹配value,remove(key,value)需要匹配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);
//如果是单链表的第一个节点
else if (node == p)
tab[index] = node.next;
//否则,直接将当前节点的前一个节点的next指针指向下一个节点
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
//找到就返回被删除的节点
return node;
}
}
return null;
}
②remove(key, value)方法删除具体的键值对。只需要将参数matchValue设置为true,表明key和value都要匹配。
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
2.7 查找
2.7.1 查找key
(1)keySet方法获取HashMap中所有的key组成的集合KeySet(不重复)。
public Set<K> keySet() {
//keySet定义于HashMap的抽象父类AbstractMap中
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
KeySet的实现与普通的hashSet类似,区别在于使用了定制的迭代器KeyIterator,2.9节介绍该迭代器的实现。
final class KeySet extends AbstractSet<K> {
...
public final Iterator<K> iterator() { return new KeyIterator(); }
...
}
一个值得注意地方是,HashMap中发生的改变,将直接影响keySet的内容;反之亦然。比如:
HashMap<String, Integer> map = new HashMap<>(4, 0.75f);
//添加三个键值对
map.put("a",97);
map.put("e",101);
map.put("b",98);
//获取keySet
Set<String> keySet = map.keySet();
//使用迭代器,遍历keySet的所有元素,得到 a、e、b
Iterator<String> it = keySet.iterator();
while(it.hasNext()){
String key = it.next();
System.out.println(key);
}
System.out.println("***********");
//向容器中添加一个键值对
map.put("c",99);
//重新遍历keySet,得到 a、b、c、e
it = keySet.iterator();
while(it.hasNext()){
String key = it.next();
System.out.println(key);
}
另一个值得注意的地方是,如果在迭代器遍历keySet的过程中,修改了map,那么迭代器获取的结果将是不确定的。因为在修改map的过程中可能发生扩容,各个节点的存储位置将可能发生变化。
(2)containsKey方法判断map中是否包含给定的key。
public boolean containsKey(Object key) {
//调用getNode方法
return getNode(hash(key), key) != null;
}
getNode方法根据传入的hash值和key对象,查找对应的节点。
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//找到给定hash值所应该存储的索引位置,first指向该位置第一个节点
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.2 查找value
(1)values()方法获取map中所有的value组成的集合Values(可重复)。
public Collection<V> values() {
Collection<V> vs = values;
if (vs == null) {
vs = new Values();
values = vs;
}
return vs;
}
同样,Values与普通的Collection容器类似,只是使用了定制迭代器。
public final Iterator<V> iterator() { return new ValueIterator(); }
map中发生的改变,将直接影响Values。
如果在迭代器遍历values的过程中,修改了map,同样,迭代器获取的结果将是不确定的。
(2)containsValue方法判断map中是否包含一个或多个value对象。由于只知道value的情况下,不能直接通过hash函数计算出所在的索引位置,所以要进行全表遍历。
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
//对hash表的每个位置进行遍历
for (Node<K,V> e : tab) {
//对链表的每个节点进行遍历
for (; e != null; e = e.next) {
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}
2.7.3 查找entry(key-value)
entrySet方法获取map中所有的键值对组成的集合EntrySet(不重复)。
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
map中发生的改变,将直接影响键值对集合。
如果在迭代器遍历该集合的过程中,修改了map,同样,迭代器获取的结果将是不确定的。
获取到entrySet后,可以取得每一个Entry,这里的Entry实际上是其实现类Node。然后可以通过调用entry.getKey()和entry.getValue()取得每个Node中的key和value。
public final K getKey() { return key; }
public final V getValue() { return value; }
2.8 修改
put(key,value)方法同时可以用于修改某个key对应的value。
2.9 迭代器
KeyIterator是keySet的迭代器,其继承自HashIterator。next方法先调用HashIterator类中的nextNode方法返回下一个节点,然后取得该Node中的key
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
HashIterator类。基于hash的数据结构所使用的迭代器通常都继承了HashIterator。该类型迭代器的遍历顺序通常是按照hash表的索引顺序,依次在每个位置上进行遍历。如果遇到某个位置上使用了链表存储节点,则按照链表的方式进行遍历;如果遇到的是使用了红黑树存储节点,则按照红黑树的方式进行遍历。
abstract class HashIterator {
//没有修饰符,以下这些属性对子类可见
Node<K,V> next; // 要返回的下一个键值对
Node<K,V> current; // 当前迭代器所指向的键值对
int expectedModCount; // 期待的修改次数
int index; // 当前所在的hash表槽slot
HashIterator() {
expectedModCount = modCount;
//获取当前的hash表
Node<K,V>[] t = table;
current = next = null;
//起始时,在表索引为0的位置
index = 0;
if (t != null && size > 0) { // advance to first entry
//找到第一个存储了数据的索引位置,next指向该位置的第一个节点
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;
//e指向下一个节点
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
//链表结构下,将current指针下移一位到next,next指针也下移一位
//当next指向null时,说明当前索引位置已经没有元素了,现在需要找到一个存储了元素的索引
//next指向该索引位置的第一个节点
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
//返回当前位置的节点
return e;
}
//移除current位置的节点
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
//根据key调用removeNode方法就可以删除节点了
removeNode(p.hash, p.key, null, false, false);
expectedModCount = modCount;
}
}
ValueIterator也继承了HashIterator,在调用next获取下一个value时,调用了nextNode方法获取下一个节点,再取其value值。
final class ValueIterator extends HashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
EntryIterator也是继承了HashIterator,在调用next获取下一个节点时,直接调用了nextNode方法获取下一个节点。
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
2.10 fast-fail机制
在List源码阅读章节,介绍了fast-fail机制,参考https://blog.csdn.net/Longstar_L/article/details/111146006第2.5节fast-fail机制。java.util包下的容器类一般都存在这个错误检测机制。在本线程使用迭代器遍历期间,其他线程修改了容器或者本线程自身修改了容器(未使用迭代器修改),就会抛出ConcurrentModificationException异常。