深入淺出Java中Map介面的實現

weixin_33912445發表於2017-04-18

一、 基本介紹

java.util.Map介面,檢視定義原始碼為:

package java.util;
public interface Map<K,V> {
  ……
}

HashMap、TreeMap、Hashtable、LinkedHashMap 都是實現Map介面,均為鍵值對形式,且map的key不允許重複

二、 詳細介紹

1. HashMap

HashMap類,我在之前的文章“Java面試 HashMap、HashSet原始碼解析”中進行過詳細的說明,想要詳細瞭解的可以跳轉過去看一下。
HashMap是最常用的Map類,根據鍵的Hash值計算儲存位置,儲存鍵值對,可以根據鍵獲取對應值。具有很快的訪問速度,但是是無序的、執行緒不安全的。且HashMap不同步,如果需要執行緒同步,則需要使用ConcurrentHashMap,也可以使用Collections.synchronizedMap(HashMap map)方法讓HashMap具有同步的能力。其實是否同步,就看有沒有synchronized關鍵字。 HashMap的key有且只能允許一個null。

注:執行緒不安全(多個執行緒訪問同一個物件或實現進行更新操作時,造成資料混亂)

2. Hashtable

Hashtable繼承自Dictionary類 ,它也是無序的,但是Hashtable是執行緒安全的,同步的,即任一時刻只有一個執行緒能寫Hashtable
由此我們比較一下HashMap和Hashtable的執行效率
測試插入效率如下:

        long runCount=1000000;
        Map<Integer,Integer> hashMap = new HashMap<Integer, Integer>();
        Date dateBegin = new Date();
        for (int i = 0; i < runCount; i++) {
            hashMap.put(i, i);
        }
        Date dateEnd = new Date();
        System.out.println("HashMap插入用時為:" + (dateEnd.getTime() - dateBegin.getTime()));

        Map<Integer,Integer> hashtable = new Hashtable<Integer, Integer>();
        Date dateBegin1 = new Date();
        for (int i = 0; i < runCount; i++) {
            hashtable.put(i, i);
        }
        Date dateEnd1 = new Date();
        System.out.println("Hashtable插入用時為:" + (dateEnd1.getTime() - dateBegin1.getTime()));

執行結果為:

HashMap插入用時為:223
Hashtable插入用時為:674

如果我們將執行次數提高到20000000次,則執行時間分別為:

HashMap插入用時為:36779
Hashtable插入用時為:22632

由此可見,在資料量較小時,HashMap效率較高,但是當資料量增大,HashMap需要進行更多次的resize,這個操作會極大的降低HashMap的執行效率,因此在資料量大之後,Hashtable的執行效率更高。
而反過來重新測試讀取效率,程式碼如下:

        long runCount=1000000;
        Map<Integer,Integer> hashMap = new HashMap<Integer, Integer>();
        for (int i = 0; i < runCount; i++) {
            hashMap.put(i, i);
        }
        Date dateBegin = new Date();
        for (Integer key : hashMap.keySet()) {
            hashMap.get(key);
        }
        Date dateEnd = new Date();
        System.out.println("HashMap讀取用時為:" + (dateEnd.getTime() - dateBegin.getTime()));

        Map<Integer,Integer> hashtable = new Hashtable<Integer, Integer>();
        for (int i = 0; i < runCount; i++) {
            hashtable.put(i, i);
        }
        Date dateBegin1 = new Date();
        for (Integer key : hashtable.keySet()) {
            hashtable.get(key);
        }
        Date dateEnd1 = new Date();
        System.out.println("Hashtable讀取用時為:" + (dateEnd1.getTime() - dateBegin1.getTime()));

執行結果為:

HashMap讀取用時為:54
Hashtable讀取用時為:65

如果將數量增加到20000000,則執行結果為:

HashMap讀取用時為:336
Hashtable讀取用時為:526

由此可見,HashMap的讀取效率更高。

3. LinkedHashMap

LinkedHashMap是Map中常用的有序的兩種實現之一, 它儲存了記錄的插入順序,先進先出。
對於LinkedHashMap而言,它繼承與HashMap,底層使用雜湊表與雙向連結串列來儲存所有元素。其基本操作與父類HashMap相似,它通過重寫父類相關的方法,來實現自己的連結列表特性。LinkedHashMap採用的hash演算法和HashMap相同,但是它重新定義了陣列中儲存的元素Entry,該Entry除了儲存當前物件的引用外,還儲存了其上一個元素before和下一個元素after的引用,從而在雜湊表的基礎上又構成了雙向連結列表,效果圖如下:


5408072-177f4d49e19a48e7
雙向連結串列結構示意圖

示例程式碼如下:

        Map<Integer,Integer> linkedHashMap = new LinkedHashMap<Integer, Integer>();
        linkedHashMap.put(1, 2);
        linkedHashMap.put(3, 4);
        linkedHashMap.put(5, 6);
        linkedHashMap.put(7, 8);
        linkedHashMap.put(9, 0);
        System.out.println("linkedHashMap的值為:" + linkedHashMap);

輸出結果為:

linkedHashMap的值為:{1=2, 3=4, 5=6, 7=8, 9=0}

注:LinkedHashMap在遍歷的時候會比HashMap慢,不過有種情況例外,當HashMap容量很大,實際資料較少時,遍歷起來可能會 比LinkedHashMap慢,因為LinkedHashMap的遍歷速度只和實際資料有關,和容量無關,而HashMap的遍歷速度和他的容量有關

4. TreeMap

TreeMap實現SortMap介面,能夠把它儲存的記錄根據鍵排序,預設是按鍵值的升序排序,也可以指定排序的比較器,當用Iterator 遍歷TreeMap時,得到的記錄是排過序的。
TreeMap的排序原理是:紅黑樹演算法的實現 ,具體概念參考:點選開啟連結
它的主要實現是Comparator架構,通過比較的方式,進行一個排序,以下是TreeMap的原始碼,
比較的原始碼為:

    /**
     * Compares two keys using the correct comparison method for this TreeMap.
     */
    @SuppressWarnings("unchecked")
    final int compare(Object k1, Object k2) {
        return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
            : comparator.compare((K)k1, (K)k2);
    }

我們也可以自定義Comparator, 對TreeMap資料的排序規則進行修改,這點是LinkedHashMap不能實現的
具體程式碼如下:

        Map<String,Integer> treeMap = new TreeMap<String, Integer>();
        treeMap.put("aa", 888);
        treeMap.put("ee", 55);
        treeMap.put("dd", 777);
        treeMap.put("cc", 88);
        treeMap.put("bb", 999);
        System.out.println("使用預設排序規則,生成的結果為:" + treeMap);

        Map<String, Integer> treeMap2 = new TreeMap<String, Integer>(new Comparator<String>() {
            public int compare(String o1, String o2) {
                return o2.compareTo(o1);
            }
        });
        treeMap2.put("aa", 888);
        treeMap2.put("ee", 55);
        treeMap2.put("dd", 777);
        treeMap2.put("cc", 88);
        treeMap2.put("bb", 999);
        System.out.println("使用自定義排序規則,生成的結果為:" + treeMap2);

執行結果為:

使用預設排序規則,生成的結果為:{aa=888, bb=999, cc=88, dd=777, ee=55}
使用自定義排序規則,生成的結果為:{ee=55, dd=777, cc=88, bb=999, aa=888}

這邊可以檢視一下compareTo()的方法原始碼,內容為:

    public int compareTo(String anotherString) {
        //先得到比較值的字串長度
        int len1 = value.length;
        int len2 = anotherString.value.length;
        //得到最小長度
        int lim = Math.min(len1, len2);
        char v1[] = value;
        char v2[] = anotherString.value;

        int k = 0;
        //逐個比較字串中字元大小
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        //如果在兩個字串的最小長度內,字元均相同,則比較長度
        return len1 - len2;
    }

由此可見,當key值中儲存了Integer型別的數字時,將預設無法根據數字大小來進行排序,處理方式如下:

        Map<String,Integer> treeMap = new TreeMap<String, Integer>();
        treeMap.put("1", 888);
        treeMap.put("9", 55);
        treeMap.put("31", 777);
        treeMap.put("239", 88);
        treeMap.put("177", 999);
        System.out.println("使用預設排序規則,生成的結果為:" + treeMap);

        Map<String, Integer> treeMap2 = new TreeMap<String, Integer>(new Comparator<String>() {
            public int compare(String o1, String o2) {
                //修改比較規則,按照數字大小升序排列
                return Integer.parseInt(o1) - Integer.parseInt(o2);
            }
        });
        treeMap2.put("1", 888);
        treeMap2.put("9", 55);
        treeMap2.put("31", 777);
        treeMap2.put("239", 88);
        treeMap2.put("177", 999);
        System.out.println("使用自定義排序規則,生成的結果為:" + treeMap2);

執行結果為:

使用預設排序規則,生成的結果為:{1=888, 177=999, 239=88, 31=777, 9=55}
使用自定義排序規則,生成的結果為:{1=888, 9=55, 31=777, 177=999, 239=88}

三、 總結

  1. Map中,HashMap具有超高的訪問速度,如果我們只是在Map 中插入、刪除和定位元素,而無關執行緒安全或者同步問題,HashMap 是最好的選擇。
  2. 如果考慮執行緒安全或者寫入速度的話,可以使用HashTable
  3. 如果想要按照存入資料先入先出的進行讀取。 那麼使用LinkedHashMap
  4. 如果需要讓Map按照key進行升序或者降序排序,那就用TreeMap

參照

深入淺出 Map 的實現(HashMap、HashTable、LinkedHashMap、TreeMap)

相關文章