Java集合篇:Map集合的幾種遍歷方式及性能測試

一、寫在前面

	好久不見。
	一轉眼,新的一年已經走過了兩個月,距離上次更新博客也相隔好幾個月,原本打算月更的我,也因爲工作
	和懶散,走上了拖更之路。
	不過,我小宗又回來了!

今天,讓我們來探究一個老生常談的問題:Map集合的遍歷。這同時也是許多Java初學者必須要掌握的基礎知識,網上也有很多大神很多博客對此進行講解。此博文從知識層面上借鑑之,並記錄於2021年2月3日16:13:15。


二、正式開始探究之旅

1. Map集合一共有多少種遍歷方式呢?

Map集合主要有三種遍歷方式:keySet()、entrySet()、values()。但是,如果從API層面上進行細分的話有7種。這三種各自都有兩種形式,for循環和Iterator迭代。還有最後一種,如果你是JDK8以上版本,還可以使用Lambda表達式forEach遍歷

2.那這幾種遍歷方式的具體用法是啥樣的呢?

下面,我使用IDEA新建一個項目,進行demo編寫演示。具體的IDEA操作不屬於本文的研究重點,略去不表。直接在test下面寫測試案例。

2.1 keySet()方式遍歷-------for循環

		//2.1 keySet()方式遍歷-------for循環
        long keySetForStartTime = System.nanoTime();
        for (String key : map.keySet()) {
   
    
            map.get(key);
        }
        long keySetForEndTime = System.nanoTime();

2.2 keySet()方式遍歷-------Iterator迭代

		//2.2 keySet()方式遍歷-------Iterator迭代
        long keySetIteratorStartTime = System.nanoTime();
        Iterator<String> iterator1 = map.keySet().iterator();
        while (iterator1.hasNext()) {
   
    
            String key = iterator1.next();
            map.get(key);
        }
        long keySetIteratorEndTime = System.nanoTime();

2.3 entrySet()方式遍歷-------for循環

		long entrySetForStartTime = System.nanoTime();
        for (Map.Entry<String, String> entry : map.entrySet()) {
   
    
            entry.getKey();
            entry.getValue();
        }
        long entrySetForEndTime = System.nanoTime();

2.4 entrySet()方式遍歷-------Iterator迭代

		long entrySetIteratorStartTime = System.nanoTime();
        Iterator<Map.Entry<String, String>> iterator2 = map.entrySet().iterator();
        while (iterator2.hasNext()) {
   
    
            Map.Entry<String, String> entry = iterator2.next();
            entry.getKey();
            entry.getValue();
        }
        long entrySetIteratorEndTime = System.nanoTime();

2.5 values()方式遍歷-------for循環

		long valuesForStartTime = System.nanoTime();
        Collection<String> values = map.values();
        for (String value : values) {
   
    
            //.....
        }
        long valuesForEndTime = System.nanoTime();

2.6 values()方式遍歷-------Iterator迭代

		//2.6 values()方式遍歷-------Iterator迭代
        long valuesIteratorStartTime = System.nanoTime();
        Iterator<String> iterator3 = map.values().iterator();
        while (iterator3.hasNext()) {
   
    
            String value = iterator3.next();
        }
        long valuesIteratorEndTime = System.nanoTime();

2.7 JDK8-------Lambda表達式forEach遍歷

		//2.7 JDK8-------Lambda表達式forEach遍歷
        long forEachStartTime = System.nanoTime();
        map.forEach((key, value) -> {
   
    
            //......
        });
        long forEachEndTime = System.nanoTime();

JDK8Lambda表達式的forEach方法,其實就是一種語法糖,讓你的代碼更加簡潔,使用更加方便,深入源碼,我們可以很輕易的發現,它其實就是對entrySet遍歷方式的一種包裝而已。不信你看下面我貼的forEach源碼。

forEach源碼。本方法since1.8版本

     * @param action The action to be performed for each entry
     * @throws NullPointerException if the specified action is null
     * @throws ConcurrentModificationException if an entry is found to be
     * removed during iteration
     * @since 1.8
     */
    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) {
   
    
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
            action.accept(k, v);
        }
    }

3. 那這幾種遍歷方式的性能如何呢?哪一種更推薦使用呢?

3.1首先,我們來準備一下測試數據

我們實例化一個HashMap對象,往Map中添加一百萬條數據,以此作爲測試數據。

		//準備Map,裝入測試數據
        Map<String, String> map = new HashMap<>();
        //往map裏插入一百萬數據,作爲測試數據,別怕,家裏就這條件
        for (int i = 0; i < 1000000; i++) {
   
    
            map.put(String.valueOf(i), "我是第" + i + "名");
        }

3.2打印各種遍歷方式遍歷測試數據的執行時間

由於在上一部分的API使用的代碼中,我們已經穿插進去計算執行時間的代碼,下面我們可以直接打印時間差,作爲每種遍歷方式的執行時間,雖然不是很嚴格,但是也能從結果中看得比較明顯。

		System.out.println("keySet()方式遍歷-------for循環用時:" + (keySetForEndTime - keySetForStartTime) / 1000000 + "毫秒");
        System.out.println("keySet()方式遍歷-------Iterator迭代用時:" + (keySetIteratorEndTime - keySetIteratorStartTime) / 1000000 + "毫秒");
        System.out.println("entrySet()方式遍歷-------for循環用時:" + (entrySetForEndTime - entrySetForStartTime) / 1000000 + "毫秒");
        System.out.println("entrySet()方式遍歷-------Iterator迭代用時:" + (entrySetIteratorEndTime - entrySetIteratorStartTime) / 1000000 + "毫秒");
        System.out.println("values()方式遍歷-------for循環用時:" + (valuesForEndTime - valuesForStartTime) / 1000000 + "毫秒");
        System.out.println("values()方式遍歷-------Iterator迭代用時:" + (valuesIteratorEndTime - valuesIteratorStartTime) / 1000000 + "毫秒");
        System.out.println("JDK8-------Lambda表達式forEach遍歷用時:" + (forEachEndTime - forEachStartTime) / 1000000 + "毫秒");

3.3測試結果

測試結果
結果分析:
直接拋出結論,entrySet()方式比keySet()方式效率更高,在忽略其他條件下,對於同一種遍歷方式而言,Iterator迭代比for循環效率高。

當然,上述的結論只是說出了一半。其實是分兩種情況的。在元素數量大的情況下,entrySet()性能確實是優於keySet()的,越大越明顯。同樣的,在小數據量的情況下,keySet()效率更高一點。

爲啥大數據量時,entrySet()效率高呢?

其實,keySet()遍歷,其實是相當於遍歷了兩次,第一次是轉換爲Iterator對象,第二次纔是根據key從Map中取出對應的value值。而entrySet()轉成Entry對象,只遍歷一次。
當然,還有其他的一些原因,比如,map.get(key),這一操作註定了是計算密集型操作,很耗費CPU,在此不再過多說明。

values()方式的說明

values(),顧名思義,它得到的是Map中value的集合,因此,想要獲取value對應的key值比較困難,因此使用上還是看需求。

在日常的開發工作中推薦使用哪一種遍歷方式呢?

直接說結論:推薦使用entrySet()遍歷方式,這依然是不二之選。並不是很建議使用keySet方式。如果項目是JDK8以上的版本,直接使用forEach吧,底層原理一樣,語法更好更簡潔,何樂而不爲呢?


三、寫在最後

雖然本文比較基礎,但對於初學者而言,依然是比較重要的一節課,畢竟這種數據結構的使用在日常的項目開發中,不可或缺。算是寫了一篇水文,也算是延續繼續更下去的習慣吧。

後續有時間可能會更一篇Java的數據結構,LIst、Map、Set等的底層原理以及JDK7、8版本底層的升級。

也有可能會研究一下Redis的使用。

誰又說的準呢?

畢竟下一次更文,還不知道呢。哈哈

我是小宗,Java學習,我和你一樣,一直在路上。加油!

四、附錄–全部測試源碼

@Test
    void testMap() {
   
     
        //準備Map,裝入測試數據
        Map<String, String> map = new HashMap<>();
        //往map裏插入一百萬數據,作爲測試數據,別怕,家裏就這條件
        for (int i = 0; i < 1000000; i++) {
   
     
            map.put(String.valueOf(i), "我是第" + i + "名");
        }

        //2.1 keySet()方式遍歷-------for循環
        long keySetForStartTime = System.nanoTime();
        for (String key : map.keySet()) {
   
     
            map.get(key);
        }
        long keySetForEndTime = System.nanoTime();

        //2.2 keySet()方式遍歷-------Iterator迭代
        long keySetIteratorStartTime = System.nanoTime();
        Iterator<String> iterator1 = map.keySet().iterator();
        while (iterator1.hasNext()) {
   
     
            String key = iterator1.next();
            map.get(key);
        }
        long keySetIteratorEndTime = System.nanoTime();


        //2.3 entrySet()方式遍歷-------for循環
        long entrySetForStartTime = System.nanoTime();
        for (Map.Entry<String, String> entry : map.entrySet()) {
   
     
            entry.getKey();
            entry.getValue();
        }
        long entrySetForEndTime = System.nanoTime();

        //2.4 entrySet()方式遍歷-------Iterator迭代
        long entrySetIteratorStartTime = System.nanoTime();
        Iterator<Map.Entry<String, String>> iterator2 = map.entrySet().iterator();
        while (iterator2.hasNext()) {
   
     
            Map.Entry<String, String> entry = iterator2.next();
            entry.getKey();
            entry.getValue();
        }
        long entrySetIteratorEndTime = System.nanoTime();

        //2.5 values()方式遍歷-------for循環
        long valuesForStartTime = System.nanoTime();
        Collection<String> values = map.values();
        for (String value : values) {
   
     
            //.....
        }
        long valuesForEndTime = System.nanoTime();

        //2.6 values()方式遍歷-------Iterator迭代
        long valuesIteratorStartTime = System.nanoTime();
        Iterator<String> iterator3 = map.values().iterator();
        while (iterator3.hasNext()) {
   
     
            String value = iterator3.next();
        }
        long valuesIteratorEndTime = System.nanoTime();

        //2.7 JDK8-------Lambda表達式forEach遍歷
        long forEachStartTime = System.nanoTime();
        map.forEach((key, value) -> {
   
     
            //......
        });
        long forEachEndTime = System.nanoTime();


        System.out.println("keySet()方式遍歷-------for循環用時:" + (keySetForEndTime - keySetForStartTime) / 1000000 + "毫秒");
        System.out.println("keySet()方式遍歷-------Iterator迭代用時:" + (keySetIteratorEndTime - keySetIteratorStartTime) / 1000000 + "毫秒");
        System.out.println("entrySet()方式遍歷-------for循環用時:" + (entrySetForEndTime - entrySetForStartTime) / 1000000 + "毫秒");
        System.out.println("entrySet()方式遍歷-------Iterator迭代用時:" + (entrySetIteratorEndTime - entrySetIteratorStartTime) / 1000000 + "毫秒");
        System.out.println("values()方式遍歷-------for循環用時:" + (valuesForEndTime - valuesForStartTime) / 1000000 + "毫秒");
        System.out.println("values()方式遍歷-------Iterator迭代用時:" + (valuesIteratorEndTime - valuesIteratorStartTime) / 1000000 + "毫秒");
        System.out.println("JDK8-------Lambda表達式forEach遍歷用時:" + (forEachEndTime - forEachStartTime) / 1000000 + "毫秒");

    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章