1. 程式人生 > >Java列舉型別(enum)-5

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,鍵都有一個對應的索引,根據索引直接訪問和操作其鍵陣列和值陣列,由於操作都是陣列,因此效率很高。