版權聲明:本文系博主原創,未經博主許可,禁止轉載。保留所有權利。
引用網址:https://www.cnblogs.com/zhizaixingzou/p/10050579.html
目錄

1. 枚舉
1.1. 枚舉的概念
在我們網購的過程中,訂單在某個時刻將始終處於幾種狀態中的一種,它們分別為待付款、已取消、已付款、已發貨、交易完成、已退貨等。類似這樣的例子還有很多,即在描述某個事物時,它的屬性總是可列舉並且可能值是固定且有窮的,所謂枚舉,就是列舉出所有可能的值的過程。可以看到,枚舉是個動詞,然而其要義則是得到目標集合,所以直接將此目標集合划為重點,也叫枚舉了,此時它是一個名詞,用來描述事物屬性的范圍。
1.2. 枚舉的引入
枚舉類型是在JDK1.5引入的。
在此之前,描述枚舉的現象是通過常量實現的,如仍以訂單狀態為例,則1代表待付款、2代表已取消、3代表已付款、4代表已發貨、5代表交易完成、6代表已退貨等。總之,我們要將枚舉在整數范圍內做一個映射,並不直觀。作為彌補,往往要寫注釋或文檔,描述什么數字代表什么狀態。
我們知道,程序設計的要點在於更精確地模擬現實世界,而引入枚舉達到了這個目的。引入后,我們只需要將可能用到的值集合在一起,而不用映射一個更大范圍的集合,而且描述上也更加地體現自注釋,因而也更好,這就是引入枚舉的初衷。
1.3. 枚舉的常規使用
這里定義一個打印接口。
1 package com.cjw.learning.enumeration; 2 3 public interface Printer { 4 void println(); 5 }
這里為前面講的訂單狀態定義一個枚舉類型。
1 package com.cjw.learning.enumeration; 2 3 public enum OrderStatus implements Printer { 4 UNPAID("unpaid", "goods are ready, wait to be paid"), 5 CANCELED("canceled", "goods are ready, but canceled at last"), 6 PAID("paid", "goods are payed"), 7 SHIPPED("shipped", "goods are in the way"), 8 DOWN("down", "goods are accepted by buyer"), 9 REFUNDED("refunded", "goods are returned for some reason"); 10 11 private String id; 12 private String description; 13 14 OrderStatus(String id, String description) { 15 this.id = id; 16 this.description = description; 17 } 18 19 public static OrderStatus fromId(String id) { 20 for (OrderStatus orderStatus : values()) { 21 if (orderStatus.id.equals(id)) { 22 return orderStatus; 23 } 24 } 25 return null; 26 } 27 28 public String getId() { 29 return id; 30 } 31 32 public String getDescription() { 33 return description; 34 } 35 36 public void println() { 37 System.out.println(toString()); 38 } 39 40 @Override 41 public String toString() { 42 return "OrderStatus [ id = " + id + ", description = " + description + "]"; 43 } 44 }
枚舉的使用。
1 package com.cjw.learning.enumeration; 2 3 public class Demo01 { 4 private static void process(OrderStatus orderStatus) { 5 switch (orderStatus) { 6 case UNPAID: 7 // do something. 8 break; 9 case CANCELED: 10 // do something. 11 break; 12 case PAID: 13 // do something. 14 break; 15 case SHIPPED: 16 // do something. 17 break; 18 case DOWN: 19 // do something. 20 break; 21 case REFUNDED: 22 // do something. 23 break; 24 default: 25 throw new IllegalArgumentException("Unknown status"); 26 } 27 } 28 29 public static void main(String[] args) { 30 process(OrderStatus.fromId(args[0])); 31 } 32 }
枚舉可以實現接口。
如果枚舉類型要添加自定義方法,則枚舉常量的定義必須放在最前面。
枚舉常量的標識符全大寫,如果是幾個單詞組成則單詞間使用下划線分隔。
對於枚舉值的判斷,使用switch是極為自然的,此語法在JDK1.6引入。
枚舉的公共基類是默認的,是java.lang.Enum。
枚舉的構造方法默認是私有的。
每個枚舉常量還可以重寫父類的方法,定義自己的方法,只是此時的枚舉常量實際上是枚舉的子類了,而且是匿名子類。

1.4. 枚舉的公共基類java.lang.Enum
1.4.1. 源碼解析
Enum定義如下。
1 public abstract class Enum<E extends Enum<E>> 2 implements Comparable<E>, Serializable {
枚舉實現了Comparable接口,不同枚舉比較的前提是兩個枚舉值同類型,比較的結果依照其在枚舉類型中定義的順序。
1 public final int compareTo(E o) { 2 Enum<?> other = (Enum<?>)o; 3 Enum<E> self = this; 4 if (self.getClass() != other.getClass() && // optimization 5 self.getDeclaringClass() != other.getDeclaringClass()) 6 throw new ClassCastException(); 7 return self.ordinal - other.ordinal; 8 }
判斷兩個同類型的枚舉值是否相等,可以直接用==號,equals方法等價。
1 public final boolean equals(Object other) { 2 return this==other; 3 }
枚舉常量是單例的,為了保證這點,枚舉直接不支持克隆方法。
1 protected final Object clone() throws CloneNotSupportedException { 2 throw new CloneNotSupportedException(); 3 }
根據枚舉類型和枚舉值名稱得到枚舉常量。
1 public static <T extends Enum<T>> T valueOf(Class<T> enumType, 2 String name) { 3 T result = enumType.enumConstantDirectory().get(name); 4 if (result != null) 5 return result; 6 if (name == null) 7 throw new NullPointerException("Name is null"); 8 throw new IllegalArgumentException( 9 "No enum constant " + enumType.getCanonicalName() + "." + name); 10 }
進一步跟蹤代碼,返回值乃是根據枚舉值名稱查詢Map<枚舉值名稱, 枚舉常量>得到。
1 Map<String, T> enumConstantDirectory() { 2 if (enumConstantDirectory == null) { 3 T[] universe = getEnumConstantsShared(); 4 if (universe == null) 5 throw new IllegalArgumentException( 6 getName() + " is not an enum type"); 7 Map<String, T> m = new HashMap<>(2 * universe.length); 8 for (T constant : universe) 9 m.put(((Enum<?>)constant).name(), constant); 10 enumConstantDirectory = m; 11 } 12 return enumConstantDirectory; 13 }
而此Map實際上是通過指定的枚舉類型的反射調用values方法得到的數組轉換而來。
1 T[] getEnumConstantsShared() { 2 if (enumConstants == null) { 3 if (!isEnum()) return null; 4 try { 5 final Method values = getMethod("values"); 6 java.security.AccessController.doPrivileged( 7 new java.security.PrivilegedAction<Void>() { 8 public Void run() { 9 values.setAccessible(true); 10 return null; 11 } 12 }); 13 @SuppressWarnings("unchecked") 14 T[] temporaryConstants = (T[])values.invoke(null); 15 enumConstants = temporaryConstants; 16 } 17 // These can happen when users concoct enum-like classes 18 // that don't comply with the enum spec. 19 catch (InvocationTargetException | NoSuchMethodException | 20 IllegalAccessException ex) { return null; } 21 } 22 return enumConstants; 23 }
1.4.2. java.lang.Enum的使用實例
1 package com.cjw.learning.enumeration; 2 3 public class Demo02 { 4 public static void main(String[] args) { 5 for (OrderStatus orderStatus : OrderStatus.values()) { 6 System.out.println(orderStatus.name() + ": " + orderStatus.ordinal()); 7 8 } 9 10 OrderStatus unpaid = OrderStatus.fromId("paid"); 11 System.out.println(unpaid == OrderStatus.PAID); 12 // 這里調用equals方法時讓枚舉常量在前,而非枚舉變量在前,乃是一種好習慣:可以避免拋空指針異常。 13 System.out.println(OrderStatus.PAID.equals(unpaid)); 14 15 System.out.println(OrderStatus.UNPAID.compareTo(OrderStatus.PAID)); 16 System.out.println(OrderStatus.PAID.compareTo(OrderStatus.PAID)); 17 18 System.out.println(Enum.valueOf(OrderStatus.class, "PAID")); 19 System.out.println(OrderStatus.valueOf("PAID")); 20 } 21 }
輸出如下:

1.5. 專為枚舉設計的集合類
1.5.1. Key為枚舉的Map:java.util.EnumMap
1.5.1.1. Key為枚舉類型
它的Key必須來自同一個枚舉類型,當Map創建時指定。
1 public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> 2 implements java.io.Serializable, Cloneable
1.5.1.2. 存儲結構
如下分別存儲枚舉類型、所有枚舉常量數組、Map的值集合(個數與枚舉常量數一致,數組索引號即枚舉常量的定義順序)、Map當前的大小(為某個枚舉常量放入了值,則加一,反之則減一)。
1 private final Class<K> keyType; 2 private transient K[] keyUniverse; 3 private transient Object[] vals; 4 private transient int size = 0;
1.5.1.3. 創建實例
定義實例。
1 public EnumMap(Class<K> keyType) { 2 this.keyType = keyType; 3 keyUniverse = getKeyUniverse(keyType); 4 vals = new Object[keyUniverse.length]; 5 }
此時會初始化Key的類型等值,可以看到值集合全為null,表示Map為空。
1.5.1.4. 存入鍵值對(null封裝的巧妙)
在添加鍵值對時,首先檢查Key是否是創建時指定的枚舉類型,接着將值封裝后存入指定的位置,對應的舊值如果是null,則大小加一,表示存入值了。
1 public V put(K key, V value) { 2 typeCheck(key); 3 4 int index = key.ordinal(); 5 Object oldValue = vals[index]; 6 vals[index] = maskNull(value); 7 if (oldValue == null) 8 size++; 9 return unmaskNull(oldValue); 10 }
可以看到,一般的Map的大小是動態增減的,而EnumMap不是,而是一開始就創建了指定枚舉常量數的數組。區分是否添加了值,是通過為null還是不為null來的,而真正存放null,則是通過封裝為NULL來表示的,這是一個技巧。
1 private static final Object NULL = new Object() { 2 public int hashCode() { 3 return 0; 4 } 5 6 public String toString() { 7 return "java.util.EnumMap.NULL"; 8 } 9 };
存入時,如果為null,則存NULL,以區別於沒有存放null值。
1 private Object maskNull(Object value) { 2 return (value == null ? NULL : value); 3 }
讀取時,如果得到的NULL,則表示之前存入的是null值。
1 private V unmaskNull(Object value) { 2 return (V)(value == NULL ? null : value); 3 }
1.5.1.5. 讀取鍵對應的值
首先檢查鍵的類型,如果不對,則返回null。否則返回值,如果沒有為它賦過值,它為null,如果為它賦過值,如果是NULL,則還是返回null,否則返回對應的值。
1 public V get(Object key) { 2 return (isValidKey(key) ? 3 unmaskNull(vals[((Enum<?>)key).ordinal()]) : null); 4 }
1.5.1.6. 刪除鍵對應的值
1 public V remove(Object key) { 2 if (!isValidKey(key)) 3 return null; 4 int index = ((Enum<?>)key).ordinal(); 5 Object oldValue = vals[index]; 6 vals[index] = null; 7 if (oldValue != null) 8 size--; 9 return unmaskNull(oldValue); 10 }
1.5.1.7. 總結
此Map的結構和原理在上面已經講清,其他方法都自然而然可以推導。
清除:
1 public void clear() { 2 Arrays.fill(vals, null); 3 size = 0; 4 }
獲取Map大小:
1 public int size() { 2 return size; 3 }
是否包含指定的鍵:
1 public boolean containsKey(Object key) { 2 return isValidKey(key) && vals[((Enum<?>)key).ordinal()] != null; 3 }
是否包含指定的值:
1 public boolean containsValue(Object value) { 2 value = maskNull(value); 3 4 for (Object val : vals) 5 if (value.equals(val)) 6 return true; 7 8 return false; 9 }
可以看到,此Map內部呈現為數組,數組是大小固定為枚舉常量數的。通過元素值是否為null來區分是否為指定的枚舉常量鍵指定了值,而在此前提下為了支持null值的存儲,對null進行了封裝,正如其方面名一樣,“給null帶上面具”。總之,EnumMap相對一般Map是緊湊而高效的。
另外,多提一句,此類被聲明為可序列化的,但它使用了名為序列化代理的特性,關於該特性的具體描述見講解序列化的章節。
1.5.2. 只存枚舉的Set:java.util.EnumSet
1.5.2.1. EnumSet的使用實例
下面是EnumSet的主要使用方法(當然,它本身是個Set,因而也有Set的方法,只是這里只講專屬於它的方法)。
1 package com.cjw.learning.enumeration; 2 3 import java.util.Collection; 4 import java.util.EnumSet; 5 import java.util.stream.Collectors; 6 7 public class Demo03 { 8 private static Collection<String> listNames(EnumSet<OrderStatus> orderStatuses) { 9 return orderStatuses.stream().map(OrderStatus::name).collect(Collectors.toList()); 10 } 11 12 public static void main(String[] args) { 13 EnumSet<OrderStatus> enumSet01 = EnumSet.noneOf(OrderStatus.class); 14 System.out.println("noneOf:" + listNames(enumSet01)); 15 16 enumSet01.add(OrderStatus.UNPAID); 17 enumSet01.add(OrderStatus.PAID); 18 System.out.println("add:" + listNames(enumSet01)); 19 20 EnumSet<OrderStatus> enumSet02 = EnumSet.complementOf(enumSet01); 21 System.out.println("complementOf:" + listNames(enumSet02)); 22 23 EnumSet<OrderStatus> enumSet03 = EnumSet.allOf(OrderStatus.class); 24 System.out.println("allOf:" + listNames(enumSet03)); 25 26 EnumSet<OrderStatus> enumSet04 = EnumSet.copyOf(enumSet03); 27 System.out.println("copyOf:" + listNames(enumSet04)); 28 29 EnumSet<OrderStatus> enumSet05 = EnumSet.of(OrderStatus.UNPAID, OrderStatus.CANCELED, OrderStatus.PAID); 30 System.out.println("of:" + listNames(enumSet05)); 31 32 EnumSet<OrderStatus> enumSet06 = EnumSet.range(OrderStatus.CANCELED, OrderStatus.DOWN); 33 System.out.println("range:" + listNames(enumSet06)); 34 } 35 }
程序輸出:

1.5.2.2. EnumSet的實現原理
1 public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E> 2 implements Cloneable, java.io.Serializable
EnumSet含如下字段:
1 final Class<E> elementType; 2 final Enum<?>[] universe;
其中,elementType指存放的枚舉的類型,universe則為存儲枚舉的數組,EnumSet一創建就存了全部的枚舉常量。后面看到,Set上的增減並不在這個字段上體現。
下面是創建空EnumSet的方法。
1 public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) { 2 Enum<?>[] universe = getUniverse(elementType); 3 if (universe == null) 4 throw new ClassCastException(elementType + " not an enum"); 5 6 if (universe.length <= 64) 7 return new RegularEnumSet<>(elementType, universe); 8 else 9 return new JumboEnumSet<>(elementType, universe); 10 }
下面是創建包含全部枚舉常量EnumSet的方法。
1 public static <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) { 2 EnumSet<E> result = noneOf(elementType); 3 result.addAll(); 4 return result; 5 }
可以看到EnumSet的具體實現是由兩個子類承擔的。
如果枚舉常量的個數不超過64個,就使用RegularEnumSet子類實現。

這里的elements是一個64位存儲的long型,它就是實際用於Set存儲的字段。如果Set中含有某個枚舉常量,該枚舉常量的定義序數為ordinal,那么該long型值第ordinal位低位為1。
而如果枚舉常量的個數超過64個,就使用JumboEnumSet子類實現。它存儲枚舉常量使用的是如下字段:
1 private long elements[];
可以看到,是long的數組。它仍舊是一個比特對應一個枚舉常量。
1.6. 枚舉的實現原理
通過OrderStatus的字節碼,可以看到枚舉類型包含了該類型的幾個常量,和這些常量的數組$VALUES,$VALUES編譯器生成的私有靜態字段。

也看到生成了公共方法values,它返回$VALUES的克隆結果。
而根據枚舉常量名稱生成枚舉常量的valueOf方法,則借助了Enum的valueOf方法。

另外,靜態初始化方法會收集枚舉類型中定義的靜態語句執行,該方法創建了指定的幾個枚舉常量,並將它們存進數組$VALUES。可以看到,前面幾個常量,都是生成了OrderStatus對象后,調用其含4參的<init>,是私有的構造方法,最后一個枚舉常量仍然是OrderStatus類型,但實例化用的是它的匿名子類,該匿名子類也是用含4參的<init>。

OrderStatus含4參(分別對應name、ordinal和兩個自定義的字段)的<init>,實際上內部會調用枚舉的默認私有的含2參的構造方法。

OrderStatus匿名子類含4參的<init>,最后調用OrderStatus含5參的<init>。

而OrderStatus含5參(另一參為匿名子類實例)的<init>由調用其含4參的<init>,實際上在OrderStatus中,匿名子類的實例沒有用到,所以指定為null的。

1.7. 參考資料
JDK 1.8源碼
http://blog.lichengwu.cn/java/2011/09/26/the-usage-of-enum-in-java/
