本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結
上節我們提到,如果需要一個Map的實現類,並且鍵的型別為列舉型別,可以使用HashMap,但應該使用一個專門的實現類EnumMap。
為什麼要有一個專門的類呢?我們之前介紹過列舉的本質,主要是因為列舉型別有兩個特徵,一是它可能的值是有限的且預先定義的,二是列舉值都有一個順序,這兩個特徵使得可以更為高效的實現Map介面。
我們先來看EnumMap的用法,然後看它到底是怎麼實現的。
用法
舉個簡單的例子,比如,有一批關於衣服的記錄,我們希望按尺寸統計衣服的數量。
定義一個簡單的列舉類,Size,表示衣服的尺寸:
public enum Size {
SMALL, MEDIUM, LARGE
}
複製程式碼
定義一個簡單類,Clothes,表示衣服:
class Clothes {
String id;
Size size;
public Clothes(String id, Size size) {
this.id = id;
this.size = size;
}
public String getId() {
return id;
}
public Size getSize() {
return size;
}
}
複製程式碼
有一個表示衣服記錄的列表List<Clothes>
,我們希望按尺寸統計數量,統計方法可以為:
public static Map<Size, Integer> countBySize(List<Clothes> clothes){
Map<Size, Integer> map = new EnumMap<>(Size.class);
for(Clothes c : clothes){
Size size = c.getSize();
Integer count = map.get(size);
if(count!=null){
map.put(size, count+1);
}else{
map.put(size, 1);
}
}
return map;
}
複製程式碼
大部分程式碼都很簡單,需要注意的是EnumMap的構造方法,如下所示:
Map<Size, Integer> map = new EnumMap<>(Size.class);
複製程式碼
與HashMap不同,它需要傳遞一個型別資訊,我們在37節簡單介紹過執行時型別資訊,Size.class表示列舉類Size的執行時型別資訊,Size.class也是一個物件,它的型別是Class。
為什麼需要這個引數呢?沒有這個,EnumMap就不知道具體的列舉類是什麼,也無法初始化內部的資料結構。
使用以上的統計方法也是很簡單的,比如:
List<Clothes> clothes = Arrays.asList(new Clothes[]{
new Clothes("C001",Size.SMALL),
new Clothes("C002", Size.LARGE),
new Clothes("C003", Size.LARGE),
new Clothes("C004", Size.MEDIUM),
new Clothes("C005", Size.SMALL),
new Clothes("C006", Size.SMALL),
});
System.out.println(countBySize(clothes));
複製程式碼
輸出為:
{SMALL=3, MEDIUM=1, LARGE=2}
複製程式碼
需要說明的是,EnumMap是保證順序的,輸出是按照鍵在列舉中的順序的。
除了以上介紹的構造方法,EnumMap還有兩個構造方法,可以接受一個鍵值匹配的EnumMap或普通Map,如下所示:
public EnumMap(EnumMap<K, ? extends V> m)
public EnumMap(Map<K, ? extends V> m)
複製程式碼
比如:
Map<Size,Integer> hashMap = new HashMap<>();
hashMap.put(Size.LARGE, 2);
hashMap.put(Size.SMALL, 1);
Map<Size, Integer> enumMap = new EnumMap<>(hashMap);
複製程式碼
以上就是EnumMap的基本用法,與HashMap的主要不同,一是構造方法需要傳遞型別引數,二是保證順序。
有人可能認為,對於列舉,使用Map是沒有必要的,比如對於上面的統計例子,可以使用一個簡單的陣列:
public static int[] countBySize(List<Clothes> clothes){
int[] stat = new int[Size.values().length];
for(Clothes c : clothes){
Size size = c.getSize();
stat[size.ordinal()]++;
}
return stat;
}
複製程式碼
這個方法可以這麼使用:
List<Clothes> clothes = Arrays.asList(new Clothes[]{
new Clothes("C001",Size.SMALL),
new Clothes("C002", Size.LARGE),
new Clothes("C003", Size.LARGE),
new Clothes("C004", Size.MEDIUM),
new Clothes("C005", Size.SMALL),
new Clothes("C006", Size.SMALL),
});
int[] stat = countBySize(clothes);
for(int i=0; i<stat.length; i++){
System.out.println(Size.values()[i]+": "+ stat[i]);
}
複製程式碼
輸出為:
SMALL 3
MEDIUM 1
LARGE 2
複製程式碼
可以達到同樣的目的。但,直接使用陣列需要自己維護陣列索引和列舉值之間的關係,正如列舉的優點是簡潔、安全、方便一樣,EnumMap同樣是更為簡潔、安全、方便,它內部也是基於陣列實現的,但隱藏了細節,提供了更為方便安全的介面。
實現原理
下面我們來看下具體的程式碼,從內部組成開始。
內部組成
EnumMap有如下例項變數:
private final Class<K> keyType;
private transient K[] keyUniverse;
private transient Object[] vals;
private transient int size = 0;
複製程式碼
keyType表示型別資訊,keyUniverse表示鍵,是所有可能的列舉值,vals表示鍵對應的值,size表示鍵值對個數。
構造方法
EnumMap的基本構造方法程式碼為:
public EnumMap(Class<K> keyType) {
this.keyType = keyType;
keyUniverse = getKeyUniverse(keyType);
vals = new Object[keyUniverse.length];
}
複製程式碼
呼叫了getKeyUniverse以初始化鍵陣列,其程式碼為:
private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> keyType) {
return SharedSecrets.getJavaLangAccess()
.getEnumConstantsShared(keyType);
}
複製程式碼
這段程式碼又呼叫了其他一些比較底層的程式碼,就不列舉了,原理是最終呼叫了列舉型別的values方法,values方法返回所有可能的列舉值。關於values方法,我們在列舉的本質一節介紹過其用法和實現原理,這裡就不贅述了。
儲存鍵值對
put方法的程式碼為:
public V put(K key, V value) {
typeCheck(key);
int index = key.ordinal();
Object oldValue = vals[index];
vals[index] = maskNull(value);
if (oldValue == null)
size++;
return unmaskNull(oldValue);
}
複製程式碼
首先呼叫typeCheck檢查鍵的型別,其程式碼為:
private void typeCheck(K key) {
Class keyClass = key.getClass();
if (keyClass != keyType && keyClass.getSuperclass() != keyType)
throw new ClassCastException(keyClass + " != " + keyType);
}
複製程式碼
如果型別不對,會丟擲異常。型別正確的話,呼叫ordinal獲取索引index,並將值value放入值陣列vals[index]中。EnumMap允許值為null,為了區別null值與沒有值,EnumMap將null值包裝成了一個特殊的物件,有兩個輔助方法用於null的打包和解包,打包方法為maskNull,解包方法為unmaskNull。這個特殊物件及兩個方法的程式碼為:
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) {
return (value == null ? NULL : value);
}
private V unmaskNull(Object value) {
return (V) (value == NULL ? null : value);
}
複製程式碼
根據鍵獲取值
get方法的程式碼為:
public V get(Object key) {
return (isValidKey(key) ?
unmaskNull(vals[((Enum)key).ordinal()]) : null);
}
複製程式碼
鍵有效的話,通過ordinal方法取索引,然後直接在值陣列vals裡找。isValidKey的程式碼與typeCheck類似,但是返回boolean值而不是丟擲異常,程式碼為:
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;
}
複製程式碼
檢視是否包含某個值
containsValue方法的程式碼為:
public boolean containsValue(Object value) {
value = maskNull(value);
for (Object val : vals)
if (value.equals(val))
return true;
return false;
}
複製程式碼
遍歷值陣列進行比較。
根據鍵刪除
remove方法的程式碼為:
public V remove(Object key) {
if (!isValidKey(key))
return null;
int index = ((Enum)key).ordinal();
Object oldValue = vals[index];
vals[index] = null;
if (oldValue != null)
size--;
return unmaskNull(oldValue);
}
複製程式碼
程式碼也很簡單,就不解釋了。
實現原理小結
以上就是EnumMap的基本實現原理,內部有兩個陣列,長度相同,一個表示所有可能的鍵,一個表示對應的值,值為null表示沒有該鍵值對,鍵都有一個對應的索引,根據索引可直接訪問和操作其鍵和值,效率很高。
小結
本節介紹了EnumMap的用法和實現原理,用法上,如果需要一個Map且鍵是列舉型別,則應該用它,簡潔、方便、安全,實現原理上,內部使用陣列,根據鍵的列舉索引直接操作,效率很高。
下一節,我們來看列舉型別的Set介面的實現類EnumSet,與之前介紹的Set的實現類不同,它內部沒有用對應的Map類EnumMap,而是使用了一種極為高效的方式,什麼方式呢?
未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。