上一篇文章简单的写过了HashMap的工作原理。接下来从JDK1.8的源码的角度来分析一下HashMap的遍历方式。
1,使用keySet遍历,使用map.get(key)获取value
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
keySet()方法返回的是一个new KeySet()。而KeySet是HashMap的一个内部类
final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<K> iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator<K> spliterator() { // JDK1.8新增,方便Stream流式API操作
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super K> action) { // JDK1.8新增,方便lambda形式遍历
Node<K,V>[] tab;
if (action == 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)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
这个KeySet继承自AbstractSet。AbstractSet定义了hashCode()和equals()方法,调用iterator()实现了removeAll()方法。AbstractSet继承了AbstractCollection,AbstractCollection定义了集合类操作的一些基本方法,比如toArray(),contains(e),remove(e)等等。 几乎所有的Set接口的实现类都是继承自AbstractSet。
而Set类的遍历方法,无非就是for循环、foreach、iterator三种。for循环需要的是size(),源码已有。foreach内部实现还是调用了iterator的hasNext()和next()。而iterator()从源码上看返回的是HashMap的内部类KeyIterator。来分析一下KeyIterator的源码:
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
这里有引入了nextNode()方法。nextNode()是其父类HashIterator的方法。HashIterator是HashMap的一个内部类。HashMap内部的EntryIterator、KeyIterator、ValueIterator都继承自HashIterator。
abstract class HashIterator {
Node<K,V> next; // 下一次要返回的节点
Node<K,V> current; // 当前已经返回的节点
int expectedModCount; // 检查符,参考上一边文章提到的ConcurrentModificationException
int index; // 当前hash表数组的索引值
HashIterator() {
expectedModCount = modCount; // 初始赋值,将HashMap的modCount赋值给自己
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // 寻找都第一个节点
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) { // 确定下一个返回的next节点
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); // 调用HashMap的removeNode()删除元素
expectedModCount = modCount; // removeNode()改变了modeCount的值再次赋给自己
}
}
以上分析可见,使用keySet()方便返回keySet来进行遍历,其实是通过调用iterator()返回的节点的key。
顺便看一下EntryIterator、ValueIterator的源码:
final class ValueIterator extends HashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
可见,这三个的遍历其实都是使用了nextNode()也就是node节点遍历。
2,使用EntrySet遍历。使用entry.getKey()和entry.getValue()来获取key和value。
来看一下entrySet()的源码:
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
这里引入了 new EntrySet()。EntrySet是HashMap的一个内部类。来看一下EntrySet()的源码。
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Node<K,V> candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
public final boolean remove(Object o) {
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Object value = e.getValue();
return removeNode(hash(key), key, value, true, true) != null;
}
return false;
}
public final Spliterator<Map.Entry<K,V>> spliterator() { // JDK1.8新增,方便Stream流式API操作
return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super Map.Entry<K,V>> action) { // JDK1.8新增,方便lambda表达式遍历
Node<K,V>[] tab;
if (action == 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)
action.accept(e);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
通过对之前的KeySet的分析,这段EntrySet代码的关键部分在于iterator()中引入的new EntryIterator()。而在分析KeyIterator的最后面已经引入过了EntryIterator的源码。如此的简单:
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
接下来就剩唯一一个没有看到源码的了,Map.Entry。HashMap在内部类EntryIterator中定义了内部类Entry,实现了Map.Entry接口。先来看看Map.Entry的源码:
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
// 还有JDK1.8新增的public static 的comparingByXxx的方法
}
这个Map.Entry接口的方法从方法名就知道是要干什么的。而EntryIterator中定义了内部类Entry只是实现了相关方法的具体实现而已。由于本文想阐述HashMap的遍历,就不再分析EntryIterator中定义了内部类Entry的实现源码了。以后有机会的再单独介绍HashMap整体的原理吧。
3,使用values()。由于这种方式只能得到value,不能得到key。严格意义上不能算作HashMap的遍历。
通过对keySet()的分析,很容易就能理解values()的源码:
public Collection<V> values() {
Collection<V> vs = values;
if (vs == null) {
vs = new Values();
values = vs;
}
return vs;
}
Values是HashMap的内部类,源码:
final class Values extends AbstractCollection<V> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<V> iterator() { return new ValueIterator(); }
public final boolean contains(Object o) { return containsValue(o); }
public final Spliterator<V> spliterator() {
return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super V> action) {
Node<K,V>[] tab;
if (action == 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)
action.accept(e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
4,使用JDK1.8新增的Map.foreach()。源码:
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch(IllegalStateException ise) {
// 这个异常通常意味着这个entry已不在当前的map中
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}
这种形式的遍历从源码很明白的看出使用的是entrySet()进行遍历的。这种形式的遍历通常是配合lambda表达式或者Stream流操作进行函数式遍历。
结论:
至此,最根本的两种HashMap的遍历方式的源码都已经很清楚了,有关遍历的效率的结论也很明显:使用entrySet()后entry.getKey()和entry.getValue()是最有效率的遍历。如果只需要遍历key,那使用keySet()也可。如果只需要遍历value,那是用values()就挺好。