HashMap中推薦使用entrySet方式遍歷Map類集合KV而不是keySet方式遍歷
HashMap中EntrySet和KeySet的比較
前言
閱讀《阿里巴巴Java開發手冊終極版v1.3.0》時,看到如下一句話:
【推薦】使用entrySet遍歷Map類集合KV,而不是keySet方式進行遍歷。
說明:keySet其實是遍歷了2次,一次是轉為Iterator物件,另一次是從hashMap中取出key所對應的value。而entrySet只是遍歷了一次就把key和value都放到了entry中,效率更高。如果是JDK8,使用Map.foreach方法。
正例:values()返回的是V值集合,是一個list集合物件;keySet()返回的是K值集合,是一個Set集合物件;entrySet()返回的是K-V值組合集合。
心生好奇,便來探究為什麼?
探究
有這樣一個例子,HashMap裡面存入400000個數據,來進行兩種entrySet、keySet方式遍歷,並且輸出執行時間,例子如下所示:
package vip.wulang.springdatajpa;
import org.junit.Before;
import org.junit.Test;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* @author CoolerWu on 2018/11/11.
* @version 1.0
*/
public class HashMapTest {
private HashMap<String, String> map = new HashMap<>();
@Before
public void beforeAllMethodTestInClass() {
for (int i = 0; i < 100000; i++) {
map.put("a" + i, "aa" + i);
map.put("b" + i, "bb" + i);
map.put("c" + i, "cc" + i);
map.put("d" + i, "dd" + i);
}
}
@Test
public void entrySetTest() {
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
long startTime = System.currentTimeMillis();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println(entry.getKey() + "=" + entry.getValue());
}
long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);
}
@Test
public void keySetTest() {
Iterator<String> it = map.keySet().iterator();
long startTime = System.currentTimeMillis();
while (it.hasNext()) {
String key = it.next();
System.out.println(key + "=" + map.get(key));
}
long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);
}
}
多次測試,我們可以發現方法keySetTest()時間大約為2s809ms,而entrySetTest()只有2s98ms,從測試上來說,後者執行時間小於前者。檢視HashMap.java原始碼,如下所示:
// 這是HashMap的KeyIterator、ValueIterator、EntryIterator的基本實現抽象類
abstract class HashIterator {
// 下一項返回
Node<K,V> next;
// 當前項
Node<K,V> current;
// fail-fast 機制是java集合(Collection)中的一種錯誤機制。
// 當多個執行緒對同一個集合的內容進行操作時,就可能會產生fail-fast事件。
// 例如:當某一個執行緒A通過iterator去遍歷某集合的過程中,若該集合的內容被其他執行緒所改變了;
// 那麼執行緒A訪問集合時,就會丟擲ConcurrentModificationException異常,產生fail-fast事件。
int expectedModCount;
// 當前的位置
int index;
// 無參建構函式
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
// 判斷下一項值是否為空,返回值型別為布林型別
public final boolean hasNext() {
return next != null;
}
// 獲取下一項的Node節點
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
// 如果modCount不等於expectedModCount,說明內容改變了,應執行 fail-fast 機制
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
// 先將next的引用賦值給current,並去尋找新的next。
if ((next = (current = e).next) == null && (t = table) != null) {
// 不停地尋找每一個Node節點,直到Node節點具有資料,終止迴圈並賦值給next
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
// 移除Node節點資料
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
// 如果modCount不等於expectedModCount,說明內容改變了,應執行 fail-fast 機制
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
// 呼叫HashMap.java裡面的方法,刪除該Node節點p
removeNode(hash(key), key, null, false, false);
// 把變化後的modCount賦值給expectedModCount
expectedModCount = modCount;
}
}
// KeySet的iterator()方法的返回類
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
// Values的iterator()方法的返回類
final class ValueIterator extends HashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
// EntrySet的iterator()方法的返回類
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
我們差不多已經把主要的步驟都梳理清楚了,那麼為什麼entrySet遍歷的時間 < keySet遍歷的時間?
因為entrySet遍歷的時候,存放的是Map.Entry<T, T>型別,意思是,在進行遍歷的時候已經把key、value放入其中。而keySet遍歷的時候,存放的是T型別,意思是,在進行遍歷的時候只放了key值,倘若我還需要value,就還需要使用 public V get(Object key) 方法,而這個方法具體實現如下:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
重點來了,請看這一句 getNode(hash(key), key) ,而這個方法的原始碼如下:
// 該方法是根據hash值和key值來查詢的
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
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;
// 判斷first.next是否為空指標
if ((e = first.next) != null) {
// 判斷first是否屬於紅黑樹
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 若first不是紅黑樹,則進行連結串列的遍歷,直到找到為止
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
由上可知,還進行了一次遍歷。所以keySet遍歷的時間會 > entrySet遍歷的時間。推薦使用entrySet遍歷Map類集合KV,而不是keySet方式進行遍歷。