Java8 EnumMap 源码分析
一、EnumMap 概述
EnumMap 是一個用于存儲 key 為枚舉類型的 map,底層使用數(shù)組實現(xiàn)(K,V 雙數(shù)組)。下面是其繼承結(jié)構(gòu):
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>implements java.io.Serializable, Cloneable從上面的繼承結(jié)構(gòu)上可以看出 EnumMap 的 key 必須是一個枚舉類型,而 value 沒有限制。
1.1 內(nèi)部屬性
// key 類型private final Class<K> keyType;// key 數(shù)組private transient K[] keyUniverse;// value 數(shù)組private transient Object[] vals;// 鍵值對個數(shù)private transient int size = 0;// value 為 null 時對應(yīng)的值private static final Object NULL = new Object() {public int hashCode() {return 0;}public String toString() {return "java.util.EnumMap.NULL";}};與其他類型 map 不同的是 EnumMap 底層使用雙數(shù)組來存儲 key 與 value,key 數(shù)組會在構(gòu)造函數(shù)中根據(jù) keyType 進行初始化,下面我們會看到。當 EnmumMap 的 value 為 null 時會特殊處理為一個 Object 對象。
1.2 構(gòu)造函數(shù)
EnumMap 共提供了 3 個構(gòu)造函數(shù),如下:
下面我們只來看其中一個指定類型的構(gòu)造函數(shù)。
public EnumMap(Class<K> keyType) {this.keyType = keyType;// 初始化 key 數(shù)組,getKeyUniverse 方法會計算出枚舉元素的總數(shù)并初始化 key 數(shù)組keyUniverse = getKeyUniverse(keyType);// 初始化 value 數(shù)組大小vals = new Object[keyUniverse.length];}在使用上述構(gòu)造函數(shù)初始化 EnumMap 的時候必須指定枚舉類型,上面我們已經(jīng)說過,EnumMap 會在構(gòu)造函數(shù)中初始化 key 數(shù)組,這個初始化動作是在 getKeyUniverse(keyType) 中完成的。
private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> keyType) {return SharedSecrets.getJavaLangAccess().getEnumConstantsShared(keyType);}一開始看上面的代碼可能有點懵,這怎么就初始化了 key 數(shù)組呢?在 Java 中我們可以通過 JavaLangAccess 和 SharedSecrets 來獲取 JVM 中對象實例,具體是怎么實現(xiàn)的,有興趣的可以查相關(guān)的資料了解下。
我們以 debug 形式來驗證下 key 數(shù)組是否會在構(gòu)造函數(shù)中被初始化與賦值:
首先來聲明一個枚舉類型:
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);}我們把斷點打在其構(gòu)造函數(shù)上就會看到 keyUniverse 數(shù)組被初始化了,且數(shù)組的元素順序與在枚舉類型中定義的順序一致。如下圖:
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));}二、相關(guān)源碼分析
2.1 put 方法
public V put(K key, V value) {// key 類型檢查typeCheck(key);// 獲得該 key 對應(yīng)的位置int index = key.ordinal();// 在 vals 數(shù)組中獲取 key 角標對應(yīng)的 valueObject oldValue = vals[index];// 覆蓋或設(shè)置 valuevals[index] = maskNull(value);// 如果 key 對應(yīng)的位置 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 在構(gòu)造函數(shù)中已經(jīng)被初始化了。
EnumMap 存儲鍵值對時并不會根據(jù) key 獲取對應(yīng)的哈希值,enum 本身已經(jīng)提供了一個 ordinal() 方法,該方法會返回具體枚舉元素在枚舉類中的位置(從 0 開始),因此一個枚舉元素從創(chuàng)建就已經(jīng)有了一個唯一索引與其對應(yīng),這樣就不存在哈希沖突的問題了。
如果添加的 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 添加鍵值對并沒有擴容操作,因為一個枚舉類型到底有多少元素在代碼運行階段是確定的,在構(gòu)造函數(shù)中已經(jīng)對 key 數(shù)組進行了初始化與賦值,value 數(shù)組的大小也已經(jīng)被確定。還有一個需要注意的問題,在上面的 put
方法中只對 value 進行了處理,并沒有處理 key,原因就是 key 數(shù)組在構(gòu)造函數(shù)中已經(jīng)被賦值了。
2.2 remove 方法
public V remove(Object key) {// key 類型錯誤的時候直接返回 nullif (!isValidKey(key))return null;// 根據(jù) key 計算出其在枚舉中位置int index = ((Enum<?>)key).ordinal();// 獲取對應(yīng)的 valueObject oldValue = vals[index];// value 置 null,下次 GC 回收vals[index] = null;// 如果對應(yīng)的 value 不為 null,如果添加鍵值對的時候 value 為 null,則存儲的是 NULL(Object)if (oldValue != null)size--;return unmaskNull(oldValue);}在移除鍵值對的時候會先調(diào)用 isValidKey 方法對 key 進行一次檢查:
private boolean isValidKey(Object key) {// key 為 null 直接返回 falseif (key == null)return false;// Cheaper than instanceof Enum followed by getDeclaringClassClass<?> keyClass = key.getClass();// key 類型檢查return keyClass == keyType || keyClass.getSuperclass() == keyType;}remove 方法相對來說比較簡單,這里就不總結(jié)了。
2.3 a question
從上面的源碼分析中我們知道,key 數(shù)組自從在構(gòu)造函數(shù)中完成初始化之后就沒有執(zhí)行過增刪改的操作,是不是意味著我們根據(jù)枚舉類型創(chuàng)建一個 EnumMap 之后,就算不添加任何鍵值對,也能根據(jù)其迭代器獲取所有的 key,因為 key 在構(gòu)造函數(shù)中已經(jīng)被賦值了。看下面的代碼:
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());}}結(jié)果是上面的代碼并不會輸出任何 key,原因就在于 EnumMap 的 hasNext() 方法中對 value 做了非空判斷,如下:
public boolean hasNext() {// 循環(huán)中會略過 value 數(shù)組中為 null 的情況while (index < vals.length && vals[index] == null)index++;return index != vals.length;}盡管在構(gòu)造函數(shù)中 key 數(shù)組已經(jīng)被初始化,但是如果對應(yīng)的 value 為 null,在迭代的時候也會被過濾掉。
EnumMap 相對來說比較簡單,關(guān)于源碼就介紹到這里。jdk1.8 更多的源碼分析,請點擊這里前往:jdk1.8 源碼閱讀
總結(jié)
以上是生活随笔為你收集整理的Java8 EnumMap 源码分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: lol端游双倍经验卡怎么用(lol双倍经
- 下一篇: Java8 EnumSet 源码简单分析