1. 程式人生 > >HashMap中推薦使用entrySet方式遍歷Map類集合KV而不是keySet方式遍歷

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方式進行遍歷。