本文将介绍HashMap常见的遍历方式、性能对比以及产生性能差异的原因。
遍历方式
HashMap的遍历方式主要有四种:
一、entrySet
获得一个包含entry的Set集合效率最高
Map<String, Integer> map = new HashMap<String, Integer>();
for (Entry<String, Integer> entry : map.entrySet()) {
entry.getKey();
entry.getValue();
}
二、keySet
获得一个包含key的Set集合
Map<String, Integer> map = new HashMap<String, Integer>();
for (String key : map.keySet()) {
map.get(key);
}
三、Iterator:显示调用entrySet()的迭代器
迭代器来遍历HashMap,游标每次移动都会获得一个entry<key ,value>
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
entry.getKey();
entry.getValue();
}
四、foreach
借助了Java8的新特性lambda表达式
map.forEach((k,v)-> System.out.print(k+v));
性能测试
对比这四种遍历方式在Map容量10万、100万、1000万的情况下的耗时
size | 10万 | 100万 | 1000万 |
---|---|---|---|
entrySet | 18ms | 55ms | 511ms |
keySet | 24ms | 116ms | 1484ms |
Iterator entrySet | 12ms | 75ms | 684ms |
foreach | 560ms | 3483ms | >>3483ms |
测试用的代码
public class TestHashMap {
private static final int size=1000000;
public static void main(String[] args) {
Map<String,Integer> map=new HashMap<>();
for(int i=0;i<size;i++){
String uuid= UUID.randomUUID().toString();
map.put(uuid,i);
}
long startTime=System.currentTimeMillis();
map.forEach((k,v)-> System.out.print(k+v));
/*
for (Map.Entry<String, Integer> entry : map.entrySet()) {
entry.getKey();
entry.getValue();
}
*/
long endTime=System.currentTimeMillis();
System.out.println((endTime-startTime));
}
}
从上表可以看出,性能从大到小排序:
entrySet >= Iterator >keySet > foreach
分析一下其中的原因
entrySet源码
final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() {
return nextNode(); }
}
keySet源码
final class KeyIterator extends HashIterator implements Iterator<K> {
public final K next() {
return nextNode().key; }
}
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) {
/*
遍历数组里面的链表,HashMap是数组+链表的结构;
如果当前table[index]的链表遍历完了之后,next移到table[index++]的位置,也就是下一条链表的开头
*/
do {
} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
性能entrySet > keySet的原因:
entrySet和keySet遍历HashMap都是通过nextNode()方法来遍历,一个table[index]对应一条链表(数组+链表结构)。但是keySet要获得value的话,需要额外去调用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) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//找到指定的桶,如果有hash冲突的话,table[index] 对应的就是一条链表或者树
if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {
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;
}
keySet()获取values的流程是:
- 先定位到具体的桶table[index]
- 再继续在对应的链表上循环遍历比较(如果是链表结点)
- 如果是树节点,则按照对半查找去遍历比较获取目标结点。
若hash散列算法较差造成hash冲突严重的时候(具体表现为一个table[index]对应的链表过长或红黑树的深度较深),会导致 keySet() 更加耗时。
而map.forEach((k,v)-> System.out.print(k+v))
的性能最差,应该是因为lambda表达式本身执行性能就不高( lambda表达式本身是为了编写的效率而考量的,而不是执行的效率 ),即使它的源码是按照entrySet()的方式遍历。