在 Java 编程中,HashMap
是一个非常常用的数据结构。它不仅具有高效的查找和插入性能,还有多种遍历方式可供选择。今天我们来详细剖析 HashMap
的几种常见遍历方式,并结合源码与性能测试数据进行深度分析。
HashMap 常见的遍历方式
HashMap
提供了多种遍历方式,常见的包括:
- 使用
entrySet
- 使用
keySet
- 使用
forEach
方法 - 使用
Iterator
- 使用
Stream
- 使用
parallelStream
1. 使用 entrySet
使用 entrySet
迭代是最常见和高效的方式之一。entrySet
返回的是 Map.Entry
对象的集合,每个 Entry
对象包含一个键和值。示例代码如下:
java
Map<String, Integer> map = new HashMap<>();
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}
entrySet
方式的源码解析:
java
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
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() {
return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super Map.Entry<K,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);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
从源码可以看出,entrySet
返回了一个内部类 EntrySet
,该类继承自 AbstractSet
并实现了 Map.Entry
的集合视图。
2. 使用 keySet
keySet
返回的是 HashMap
中所有键的集合。示例代码如下:
java
for (String key : map.keySet()) {
System.out.println("Key = " + key + ", Value = " + map.get(key));
}
keySet
方式的源码解析:
java
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
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 HashMap.this.removeNode(hash(key), key, null, false, true) != null; }
public final Spliterator<K> spliterator() { return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0); }
public final void forEach(Consumer<? super K> 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.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
keySet
返回了一个内部类 KeySet
,该类继承自 AbstractSet
并包含了所有键的集合视图。
3. 使用 forEach
方法
forEach
方法是 Java 8 引入的一个新特性,提供了更加简洁的遍历方式。示例代码如下:
java
map.forEach((key, value) -> System.out.println("Key = " + key + ", Value = " + value));
forEach
方式的源码解析:
java
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;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key, e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
forEach
方法直接遍历内部的 Node
数组,并对每个键值对执行指定的操作。
4. 使用 Iterator
使用 Iterator
可以更加灵活地控制遍历过程。示例代码如下:
java
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}
5. 使用 Stream
Stream
API 是 Java 8 引入的一个强大工具,用于处理集合数据。示例代码如下:
java
map.entrySet().stream().forEach(entry ->
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue()));
6. 使用 parallelStream
parallelStream
提供了并行处理的能力,适用于需要并行计算的场景。示例代码如下:
java
map.entrySet().parallelStream().forEach(entry ->
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue()));
性能分析
接下来,我们通过性能测试数据来对比这些遍历方式的效率。以下是测试结果:
无阻塞情况下的性能测试
java
Benchmark Mode Cnt Score Error Units
Test.entrySet avgt 5 288.651 ± 10.536 ns/op
Test.keySet avgt 5 584.594 ± 21.431 ns/op
Test.lambda avgt 5 221.791 ± 10.198 ns/op
Test.parallelStream avgt 5 6919.163 ± 1116.139 ns/op
从测试结果可以看出,在没有阻塞的情况下,parallelStream
性能最低,而 lambda
表达式的性能最高。
存在阻塞情况下的性能测试
在遍历过程中加入阻塞代码 Thread.sleep(10)
后,性能测试结果如下:
java
Benchmark Mode Cnt Score Error Units
Test.entrySet avgt 5 1554828440.000 ± 23657748.653 ns/op
Test.keySet avgt 5 1550612500.000 ± 6474562.858 ns/op
Test.lambda avgt 5 1551065180.000 ± 19164407.426 ns/op
Test.parallelStream avgt 5 186345456.667 ± 3210435.590 ns/op
当遍历过程中存在阻塞时,parallelStream
的性能显著提高,成为最高效的遍历方式。
总结
通过上述分析和性能测试数据,我们得出以下结论:
- 无阻塞情况:
lambda
表达式遍历方式最为高效,而parallelStream
性能最低。 - 存在阻塞情况:
parallelStream
的并行处理优势显现,成为最高效的遍历方式。
在实际应用中,选择合适的遍历方式需要根据具体的使用场景和性能需求来决定。如果需要处理大量数据且存在阻塞操作,可以考虑使用 parallelStream
来提升性能。