Java列舉型別(enum)-5
EnumMap
EnumMap基本用法
先思考這樣一個問題,現在我們有一堆size大小相同而顏色不同的資料,需要統計出每種顏色的數量是多少以便將資料錄入倉庫,定義如下列舉用於表示顏色Color:
enum Color {
GREEN,RED,BLUE,YELLOW
}
我們有如下解決方案,使用Map集合來統計,key值作為顏色名稱,value代表衣服數量,如下:
import java.util.*; public class EnumMapDemo { public static void main(String[] args){ List<Clothes> list = new ArrayList<>(); list.add(new Clothes("C001",Color.BLUE)); list.add(new Clothes("C002",Color.YELLOW)); list.add(new Clothes("C003",Color.RED)); list.add(new Clothes("C004",Color.GREEN)); list.add(new Clothes("C005",Color.BLUE)); list.add(new Clothes("C006",Color.BLUE)); list.add(new Clothes("C007",Color.RED)); list.add(new Clothes("C008",Color.YELLOW)); list.add(new Clothes("C009",Color.YELLOW)); list.add(new Clothes("C010",Color.GREEN)); //方案1:使用HashMap Map<String,Integer> map = new HashMap<>(); for (Clothes clothes:list){ String colorName=clothes.getColor().name(); Integer count = map.get(colorName); if(count!=null){ map.put(colorName,count+1); }else { map.put(colorName,1); } } System.out.println(map.toString()); System.out.println("---------------"); //方案2:使用EnumMap Map<Color,Integer> enumMap=new EnumMap<>(Color.class); for (Clothes clothes:list){ Color color=clothes.getColor(); Integer count = enumMap.get(color); if(count!=null){ enumMap.put(color,count+1); }else { enumMap.put(color,1); } } System.out.println(enumMap.toString()); } /** 輸出結果: {RED=2, BLUE=3, YELLOW=3, GREEN=2} --------------- {GREEN=2, RED=2, BLUE=3, YELLOW=3} */ }
程式碼比較簡單,我們使用兩種解決方案,一種是HashMap,一種EnumMap,雖然都統計出了正確的結果,但是EnumMap作為列舉的專屬的集合,我們沒有理由再去使用HashMap,畢竟EnumMap要求其Key必須為Enum型別,因而使用Color列舉例項作為key是最恰當不過了,也避免了獲取name的步驟,更重要的是EnumMap效率更高,因為其內部是通過陣列實現的(稍後分析),注意EnumMap的key值不能為null,雖說是列舉專屬集合,但其操作與一般的Map差不多,概括性來說EnumMap是專門為列舉型別量身定做的Map實現,雖然使用其它的Map(如HashMap)也能完成相同的功能,但是使用EnumMap會更加高效,它只能接收同一列舉型別的例項作為鍵值且不能為null,由於列舉型別例項的數量相對固定並且有限,所以EnumMap使用陣列來存放與列舉型別對應的值,畢竟陣列是一段連續的記憶體空間,根據程式區域性性原理,效率會相當高。下面我們來進一步瞭解EnumMap的用法,先看建構函式:
//建立一個具有指定鍵型別的空列舉對映。
EnumMap(Class<K> keyType)
//建立一個其鍵型別與指定列舉對映相同的列舉對映,最初包含相同的對映關係(如果有的話)。
EnumMap(EnumMap<K,? extends V> m)
//建立一個列舉對映,從指定對映對其初始化。
EnumMap(Map<K,? extends V> m)
與HashMap不同,它需要傳遞一個型別資訊,即Class物件,通過這個引數EnumMap就可以根據型別資訊初始化其內部資料結構,另外兩隻是初始化時傳入一個Map集合,程式碼演示如下:
//使用第一種構造 Map<Color,Integer> enumMap=new EnumMap<>(Color.class); //使用第二種構造 Map<Color,Integer> enumMap2=new EnumMap<>(enumMap); //使用第三種構造 Map<Color,Integer> hashMap = new HashMap<>(); hashMap.put(Color.GREEN, 2); hashMap.put(Color.BLUE, 3); Map<Color, Integer> enumMap = new EnumMap<>(hashMap);
至於EnumMap的方法,跟普通的map幾乎沒有區別,注意與HashMap的主要不同在於構造方法需要傳遞型別引數和EnumMap保證Key順序與列舉中的順序一致,但請記住Key不能為null。
EnumMap實現原理剖析
EnumMap的原始碼有700多行,這裡我們主要分析其內部儲存結構,新增查詢的實現,瞭解這幾點,對應EnumMap內部實現原理也就比較清晰了,先看資料結構和建構函式
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
implements java.io.Serializable, Cloneable
{
//Class物件引用
private final Class<K> keyType;
//儲存Key值的陣列
private transient K[] keyUniverse;
//儲存Value值的陣列
private transient Object[] vals;
//map的size
private transient int size = 0;
//空map
private static final Enum<?>[] ZERO_LENGTH_ENUM_ARRAY = new Enum<?>[0];
//建構函式
public EnumMap(Class<K> keyType) {
this.keyType = keyType;
keyUniverse = getKeyUniverse(keyType);
vals = new Object[keyUniverse.length];
}
}
EnumMap繼承了AbstractMap類,因此EnumMap具備一般map的使用方法,keyType表示型別資訊,keyUniverse表示鍵陣列,儲存的是所有可能的列舉值,vals陣列表示鍵對應的值,size表示鍵值對個數。在建構函式中通過keyUniverse = getKeyUniverse(keyType);
初始化了keyUniverse陣列的值,內部儲存的是所有可能的列舉值,接著初始化了存在Value值得陣列vals,其大小與列舉例項的個數相同,getKeyUniverse方法實現如下
//返回列舉陣列
private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> keyType) {
//最終呼叫到列舉型別的values方法,values方法返回所有可能的列舉值
return SharedSecrets.getJavaLangAccess()
.getEnumConstantsShared(keyType);
}
從方法的返回值來看,返回型別是列舉陣列,事實也是如此,最終返回值正是列舉型別的values方法的返回值,前面我們分析過values方法返回所有可能的列舉值,因此keyUniverse陣列儲存就是列舉型別的所有可能的列舉值。接著看put方法的實現
public V put(K key, V value) {
typeCheck(key);//檢測key的型別
//獲取存放value值得陣列下標
int index = key.ordinal();
//獲取舊值
Object oldValue = vals[index];
//設定value值
vals[index] = maskNull(value);
if (oldValue == null)
size++;
return unmaskNull(oldValue);//返回舊值
}
這裡通過typeCheck方法進行了key型別檢測,判斷是否為列舉型別,如果型別不對,會丟擲異常
private void typeCheck(K key) {
Class<?> keyClass = key.getClass();//獲取型別資訊
if (keyClass != keyType && keyClass.getSuperclass() != keyType)
throw new ClassCastException(keyClass + " != " + keyType);
}
接著通過int index = key.ordinal()
的方式獲取到該列舉例項的順序值,利用此值作為下標,把值儲存在vals陣列對應下標的元素中即vals[index]
,這也是為什麼EnumMap能維持與列舉例項相同儲存順序的原因,我們發現在對vals[]中元素進行賦值和返回舊值時分別呼叫了maskNull方法和unmaskNull方法
//代表NULL值得空物件例項
private static final Object NULL = new Object() {
public int hashCode() {
return 0;
}
public String toString() {
return "java.util.EnumMap.NULL";
}
};
private Object maskNull(Object value) {
//如果值為空,返回NULL物件,否則返回value
return (value == null ? NULL : value);
}
@SuppressWarnings("unchecked")
private V unmaskNull(Object value) {
//將NULL物件轉換為null值
return (V)(value == NULL ? null : value);
}
由此看來EnumMap還是允許存放null值的,但key絕對不能為null,對於null值,EnumMap進行了特殊處理,將其包裝為NULL物件,畢竟vals[]存的是Object,maskNull方法和unmaskNull方法正是用於null的包裝和解包裝的。這就是EnumMap集合的新增過程。下面接著看獲取方法
public V get(Object key) {
return (isValidKey(key) ?
unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);
}
//對Key值的有效性和型別資訊進行判斷
private boolean isValidKey(Object key) {
if (key == null)
return false;
// Cheaper than instanceof Enum followed by getDeclaringClass
Class<?> keyClass = key.getClass();
return keyClass == keyType || keyClass.getSuperclass() == keyType;
}
相對應put方法,get方法顯示相當簡潔,key有效的話,直接通過ordinal方法取索引,然後在值陣列vals裡通過索引獲取值返回。remove方法如下:
public V remove(Object key) {
//判斷key值是否有效
if (!isValidKey(key))
return null;
//直接獲取索引
int index = ((Enum<?>)key).ordinal();
Object oldValue = vals[index];
//對應下標元素值設定為null
vals[index] = null;
if (oldValue != null)
size--;//減size
return unmaskNull(oldValue);
}
非常簡單,key值有效,通過key獲取下標索引值,把vals[]對應下標值設定為null,size減一。檢視是否包含某個值,
判斷是否包含某value
public boolean containsValue(Object value) {
value = maskNull(value);
//遍歷陣列實現
for (Object val : vals)
if (value.equals(val))
return true;
return false;
}
//判斷是否包含key
public boolean containsKey(Object key) {
return isValidKey(key) && vals[((Enum<?>)key).ordinal()] != null;
}
判斷value直接通過遍歷陣列實現,而判斷key就更簡單了,判斷key是否有效和對應vals[]中是否存在該值。ok~,這就是EnumMap的主要實現原理,即內部有兩個陣列,長度相同,一個表示所有可能的鍵(列舉值),一個表示對應的值,不允許keynull,但允許value為null,鍵都有一個對應的索引,根據索引直接訪問和操作其鍵陣列和值陣列,由於操作都是陣列,因此效率很高。