Java8的EnumMap源碼分析
一、EnumMap 概述
EnumMap 是一個(gè)用于存儲(chǔ) key 為枚舉類型的 map,底層使用數(shù)組實(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 必須是一個(gè)枚舉類型,而 value 沒有限制。
1.1 內(nèi)部屬性
// key 類型 private final Class<K> keyType; // key 數(shù)組 private transient K[] keyUniverse; // value 數(shù)組 private transient Object[] vals; // 鍵值對(duì)個(gè)數(shù) private transient int size = 0; // value 為 null 時(shí)對(duì)應(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ù)組來存儲(chǔ) key 與 value,key 數(shù)組會(huì)在構(gòu)造函數(shù)中根據(jù) keyType 進(jìn)行初始化,下面我們會(huì)看到。當(dāng) EnmumMap 的 value 為 null 時(shí)會(huì)特殊處理為一個(gè) Object 對(duì)象。
1.2 構(gòu)造函數(shù)
EnumMap 共提供了 3 個(gè)構(gòu)造函數(shù),如下:
下面我們只來看其中一個(gè)指定類型的構(gòu)造函數(shù)。
public EnumMap(Class<K> keyType) { this.keyType = keyType; // 初始化 key 數(shù)組,getKeyUniverse 方法會(huì)計(jì)算出枚舉元素的總數(shù)并初始化 key 數(shù)組 keyUniverse = getKeyUniverse(keyType); // 初始化 value 數(shù)組大小 vals = new Object[keyUniverse.length]; }
在使用上述構(gòu)造函數(shù)初始化 EnumMap 的時(shí)候必須指定枚舉類型,上面我們已經(jīng)說過,EnumMap 會(huì)在構(gòu)造函數(shù)中初始化 key 數(shù)組,這個(gè)初始化動(dòng)作是在 getKeyUniverse(keyType) 中完成的。
private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> keyType) { return SharedSecrets.getJavaLangAccess() .getEnumConstantsShared(keyType); }
一開始看上面的代碼可能有點(diǎn)懵,這怎么就初始化了 key 數(shù)組呢?在 Java 中我們可以通過 JavaLangAccess 和 SharedSecrets 來獲取 JVM 中對(duì)象實(shí)例,具體是怎么實(shí)現(xiàn)的,有興趣的可以查相關(guān)的資料了解下。
我們以 debug 形式來驗(yàn)證下 key 數(shù)組是否會(huì)在構(gòu)造函數(shù)中被初始化與賦值:
首先來聲明一個(gè)枚舉類型:
enum Season { SPRING("春天"), SUMMER("夏天"), FALL("秋天"), WINTER("冬天"); private final String name; Season(String name) { this.name = name; } }
測(cè)試類:
public static void main(String[] args) throws Exception { EnumMap<Season, String> map = new EnumMap<>(Season.class); }
我們把斷點(diǎn)打在其構(gòu)造函數(shù)上就會(huì)看到 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, "寒風(fēng)凜冽的冬天"); System.out.println(map.get(Season.FALL)); }
二、相關(guān)源碼分析
2.1 put 方法
public V put(K key, V value) { // key 類型檢查 typeCheck(key); // 獲得該 key 對(duì)應(yīng)的位置 int index = key.ordinal(); // 在 vals 數(shù)組中獲取 key 角標(biāo)對(duì)應(yīng)的 value Object oldValue = vals[index]; // 覆蓋或設(shè)置 value vals[index] = maskNull(value); // 如果 key 對(duì)應(yīng)的位置 value 為 null,則表示新插入了鍵值對(duì),size++,反之表示值覆蓋 size 不變 if (oldValue == null) size++; return unmaskNull(oldValue); }
在添加鍵值對(duì)的時(shí)候會(huì)先檢查 key 的類型,如果 key 的類型不一致會(huì)拋出異常。
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 存儲(chǔ)鍵值對(duì)時(shí)并不會(huì)根據(jù) key 獲取對(duì)應(yīng)的哈希值,enum 本身已經(jīng)提供了一個(gè) ordinal() 方法,該方法會(huì)返回具體枚舉元素在枚舉類中的位置(從 0 開始),因此一個(gè)枚舉元素從創(chuàng)建就已經(jīng)有了一個(gè)唯一索引與其對(duì)應(yīng),這樣就不存在哈希沖突的問題了。
如果添加的 value 為 null 會(huì)通過 maskNull 方法特殊處理,存儲(chǔ)一個(gè) Object 對(duì)象。
private Object maskNull(Object value) { return (value == null ? NULL : value); }
如果值覆蓋的話,put 方法會(huì)返回舊的 value 值,并特殊處理 value 為 null 的情況:
private V unmaskNull(Object value) { return (V)(value == NULL ? null : value); }
EnmuMap 添加鍵值對(duì)并沒有擴(kuò)容操作,因?yàn)橐粋€(gè)枚舉類型到底有多少元素在代碼運(yùn)行階段是確定的,在構(gòu)造函數(shù)中已經(jīng)對(duì) key 數(shù)組進(jìn)行了初始化與賦值,value 數(shù)組的大小也已經(jīng)被確定。還有一個(gè)需要注意的問題,在上面的 put 方法中只對(duì) value 進(jìn)行了處理,并沒有處理 key,原因就是 key 數(shù)組在構(gòu)造函數(shù)中已經(jīng)被賦值了。
2.2 remove 方法
public V remove(Object key) { // key 類型錯(cuò)誤的時(shí)候直接返回 null if (!isValidKey(key)) return null; // 根據(jù) key 計(jì)算出其在枚舉中位置 int index = ((Enum<?>)key).ordinal(); // 獲取對(duì)應(yīng)的 value Object oldValue = vals[index]; // value 置 null,下次 GC 回收 vals[index] = null; // 如果對(duì)應(yīng)的 value 不為 null,如果添加鍵值對(duì)的時(shí)候 value 為 null,則存儲(chǔ)的是 NULL(Object) if (oldValue != null) size--; return unmaskNull(oldValue); }
在移除鍵值對(duì)的時(shí)候會(huì)先調(diào)用 isValidKey 方法對(duì) key 進(jìn)行一次檢查:
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 方法相對(duì)來說比較簡(jiǎn)單,這里就不總結(jié)了。
2.3 a question
從上面的源碼分析中我們知道,key 數(shù)組自從在構(gòu)造函數(shù)中完成初始化之后就沒有執(zhí)行過增刪改的操作,是不是意味著我們根據(jù)枚舉類型創(chuàng)建一個(gè) EnumMap 之后,就算不添加任何鍵值對(duì),也能根據(jù)其迭代器獲取所有的 key,因?yàn)?key 在構(gòu)造函數(shù)中已經(jīng)被賦值了。看下面的代碼:
public static void main(String[] args) throws Exception { EnumMap<Season, String> map = new EnumMap<>(Season.class); // 獲取迭代器對(duì)象 Iterator<Map.Entry<Season, String>> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { System.out.println(iterator.next().getKey()); } }
結(jié)果是上面的代碼并不會(huì)輸出任何 key,原因就在于 EnumMap 的 hasNext() 方法中對(duì) value 做了非空判斷,如下:
public boolean hasNext() { // 循環(huán)中會(huì)略過 value 數(shù)組中為 null 的情況 while (index < vals.length && vals[index] == null) index++; return index != vals.length; }
盡管在構(gòu)造函數(shù)中 key 數(shù)組已經(jīng)被初始化,但是如果對(duì)應(yīng)的 value 為 null,在迭代的時(shí)候也會(huì)被過濾掉。
EnumMap 相對(duì)來說比較簡(jiǎn)單,關(guān)于源碼就介紹到這里。
到此這篇關(guān)于Java8的EnumMap源碼分析的文章就介紹到這了,更多相關(guān)EnumMap源碼分析內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot使用jasypt實(shí)現(xiàn)數(shù)據(jù)庫(kù)信息脫敏的方法詳解
這篇文章主要介紹了SpringBoot使用jasypt實(shí)現(xiàn)數(shù)據(jù)庫(kù)信息的脫敏,以此來保護(hù)數(shù)據(jù)庫(kù)的用戶名username和密碼password(容易上手,詳細(xì)),文中有詳細(xì)的圖文講解和代碼示例供大家參考,需要的朋友可以參考下2024-06-06詳解JAVA抓取網(wǎng)頁(yè)的圖片,JAVA利用正則表達(dá)式抓取網(wǎng)站圖片
這篇文章主要介紹了詳解JAVA抓取網(wǎng)頁(yè)的圖片,JAVA利用正則表達(dá)式抓取網(wǎng)站圖片,非常具有實(shí)用價(jià)值,需要的朋友可以參考下。2016-12-12postgresql 實(shí)現(xiàn)16進(jìn)制字符串轉(zhuǎn)10進(jìn)制數(shù)字
這篇文章主要介紹了postgresql 實(shí)現(xiàn)16進(jìn)制字符串轉(zhuǎn)10進(jìn)制數(shù)字操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-02-02Java數(shù)據(jù)結(jié)構(gòu)之順序表詳解
這篇文章主要介紹了Java數(shù)據(jù)結(jié)構(gòu)之順序表詳解,線性表在邏輯上是線性結(jié)構(gòu),也就說是連續(xù)的一條直線。但是在物理結(jié)構(gòu)上并不一定是連續(xù)的,線性表在物理上存儲(chǔ)時(shí),通常以數(shù)組和鏈?zhǔn)浇Y(jié)構(gòu)的形式存儲(chǔ),需要的朋友可以參考下2023-07-07Java List的sort()方法改寫compare()實(shí)現(xiàn)升序,降序,倒序的案例
這篇文章主要介紹了Java List的sort()方法改寫compare()實(shí)現(xiàn)升序,降序,倒序的案例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-03-03