java集合(四)Map集合之EnumMap詳解


一、EnumMap 概述

EnumMap 是一個用於存儲 key 為枚舉類型的 map,底層使用數組實現(K,V 雙數組)。下面是其繼承結構:

public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
    implements java.io.Serializable, Cloneable

從上面的繼承結構上可以看出 EnumMap 的 key 必須是一個枚舉類型,而 value 沒有限制。

1.1 內部屬性

    // key 類型
    private final Class<K> keyType;

    // key 數組
    private transient K[] keyUniverse;

    // value 數組
    private transient Object[] vals;

    // 鍵值對個數
    private transient int size = 0;

    // value 為 null 時對應的值
    private static final Object NULL = new Object() {
        public int hashCode() {
            return 0;
        }

        public String toString() {
            return "java.util.EnumMap.NULL";
        }
    };

與其他類型 map 不同的是 EnumMap 底層使用雙數組來存儲 key 與 value,key 數組會在構造函數中根據 keyType 進行初始化,下面我們會看到。當 EnmumMap 的 value 為 null 時會特殊處理為一個 Object 對象。

1.2 構造函數

EnumMap 共提供了 3 個構造函數,如下:

 

 

 下面我們只來看其中一個指定類型的構造函數。

    public EnumMap(Class<K> keyType) {
        this.keyType = keyType;
        // 初始化 key 數組,getKeyUniverse 方法會計算出枚舉元素的總數並初始化 key 數組
        keyUniverse = getKeyUniverse(keyType);
        // 初始化 value 數組大小
        vals = new Object[keyUniverse.length];
    }

在使用上述構造函數初始化 EnumMap 的時候必須指定枚舉類型,上面我們已經說過,EnumMap 會在構造函數中初始化 key 數組,這個初始化動作是在 getKeyUniverse(keyType) 中完成的。

    private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> keyType) {
        return SharedSecrets.getJavaLangAccess()
                                        .getEnumConstantsShared(keyType);
    }

一開始看上面的代碼可能有點懵,這怎么就初始化了 key 數組呢?在 Java 中我們可以通過 JavaLangAccess 和 SharedSecrets 來獲取 JVM 中對象實例,具體是怎么實現的,有興趣的可以查相關的資料了解下。

我們以 debug 形式來驗證下 key 數組是否會在構造函數中被初始化與賦值:

首先來聲明一個枚舉類型:

enum Season {
    SPRING("春天"), SUMMER("夏天"), FALL("秋天"), WINTER("冬天");

    private final String name;

    Season(String name) {
        this.name = name;
    }
}

測試類:

    public static void main(String[] args) throws Exception {
        EnumMap<Season, String> map = new EnumMap<>(Season.class);
    }

我們把斷點打在其構造函數上就會看到 keyUniverse 數組被初始化了,且數組的元素順序與在枚舉類型中定義的順序一致。如下圖:

 

1.3 使用方式

    public static void main(String[] args) throws Exception {
        EnumMap<Season, String> map = new EnumMap<>(Season.class);
        map.put(Season.FALL, "碩果累累的秋天");
        map.put(Season.WINTER, "寒風凜冽的冬天");
        System.out.println(map.get(Season.FALL));
    }

二、相關源碼分析

2.1 put 方法

    public V put(K key, V value) {
        // key 類型檢查
        typeCheck(key);

        // 獲得該 key 對應的位置
        int index = key.ordinal();
        // 在 vals 數組中獲取 key 角標對應的 value
        Object oldValue = vals[index];
        // 覆蓋或設置 value
        vals[index] = maskNull(value);
        // 如果 key 對應的位置 value 為 null,則表示新插入了鍵值對,size++,反之表示值覆蓋 size 不變
        if (oldValue == null)
            size++;
        return unmaskNull(oldValue);
    }

在添加鍵值對的時候會先檢查 key 的類型,如果 key 的類型不一致會拋出異常。

 

    private void typeCheck(K key) {
        Class<?> keyClass = key.getClass();
        if (keyClass != keyType && keyClass.getSuperclass() != keyType)
            throw new ClassCastException(keyClass + " != " + keyType);
    }

PS: keyType 在構造函數中已經被初始化了。

EnumMap 存儲鍵值對時並不會根據 key 獲取對應的哈希值,enum 本身已經提供了一個 ordinal() 方法,該方法會返回具體枚舉元素在枚舉類中的位置(從 0 開始),因此一個枚舉元素從創建就已經有了一個唯一索引與其對應,這樣就不存在哈希沖突的問題了。

如果添加的 value 為 null 會通過 maskNull 方法特殊處理,存儲一個 Object 對象。

    private Object maskNull(Object value) {
        return (value == null ? NULL : value);
    }

如果值覆蓋的話,put 方法會返回舊的 value 值,並特殊處理 value 為 null 的情況:

    private V unmaskNull(Object value) {
        return (V)(value == NULL ? null : value);
    }

EnmuMap 添加鍵值對並沒有擴容操作,因為一個枚舉類型到底有多少元素在代碼運行階段是確定的,在構造函數中已經對 key 數組進行了初始化與賦值,value 數組的大小也已經被確定。還有一個需要注意的問題,在上面的 put
方法中只對 value 進行了處理,並沒有處理 key,原因就是 key 數組在構造函數中已經被賦值了。

2.2 remove 方法

     public V remove(Object key) {
        // key 類型錯誤的時候直接返回 null
        if (!isValidKey(key))
            return null;
        // 根據 key 計算出其在枚舉中位置
        int index = ((Enum<?>)key).ordinal();
        // 獲取對應的 value
        Object oldValue = vals[index];
        // value 置 null,下次 GC 回收
        vals[index] = null;
        // 如果對應的 value 不為 null,如果添加鍵值對的時候 value 為 null,則存儲的是 NULL(Object)
        if (oldValue != null)
            size--;
        return unmaskNull(oldValue);
    }

在移除鍵值對的時候會先調用 isValidKey 方法對 key 進行一次檢查:

    private boolean isValidKey(Object key) {
        // key 為 null 直接返回 false
        if (key == null)
            return false;

        // Cheaper than instanceof Enum followed by getDeclaringClass
        Class<?> keyClass = key.getClass();
        // key 類型檢查
        return keyClass == keyType || keyClass.getSuperclass() == keyType;
    }

remove 方法相對來說比較簡單,這里就不總結了。

2.3 a question

從上面的源碼分析中我們知道,key 數組自從在構造函數中完成初始化之后就沒有執行過增刪改的操作,是不是意味着我們根據枚舉類型創建一個 EnumMap 之后,就算不添加任何鍵值對,也能根據其迭代器獲取所有的 key,因為 key 在構造函數中已經被賦值了。看下面的代碼:

    public static void main(String[] args) throws Exception {
        EnumMap<Season, String> map = new EnumMap<>(Season.class);
        // 獲取迭代器對象
        Iterator<Map.Entry<Season, String>> iterator = map.entrySet().iterator();
        
        while (iterator.hasNext()) {
            System.out.println(iterator.next().getKey());
        }
    }

結果是上面的代碼並不會輸出任何 key,原因就在於 EnumMap 的 hasNext() 方法中對 value 做了非空判斷,如下:

        public boolean hasNext() {
            // 循環中會略過 value 數組中為 null 的情況
            while (index < vals.length && vals[index] == null)
                index++;
            return index != vals.length;
        }

盡管在構造函數中 key 數組已經被初始化,但是如果對應的 value 為 null,在迭代的時候也會被過濾掉。

EnumMap 相對來說比較簡單,關於源碼就介紹到這里。jdk1.8 更多的源碼分析,請點擊這里前往:jdk1.8 源碼閱讀


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM