什麼是列舉
什麼是列舉?說實話,在我這些年的開發生涯中,用過列舉的次數大概兩隻手都可以數的過來。當然你不能說列舉一無是處,只能說是我對 Java 理解的還不夠深刻,在可以使用列舉的時候並沒有去使用。
假設我有兩個孩子(其實不用假設),每到週末他們都不知道去上什麼輔導班。於是我就寫了一個簡單的程式來告訴他們,虛擬碼如下:
public final static int DAVID = 0;
public final static int MARRY = 1;
public void order(int who){
switch (who){
case DAVID:
System.out.println("大衛,去練武術!");
break;
case MARRY:
System.out.println("瑪麗,去練跳舞!");
break;
default:
System.out.println("今天休息");
}
}
複製程式碼
然後我告訴 David 你輸入 0,告訴 Marry 你輸入 1,就可以了。不久之後,輔導班老師就指點問候我了,您家的兩個孩子呢?這個氣的我呀,立馬回家看了看日誌,兩個孩子除了 0 和 1,其他數字都輸齊了。
由此可見,這樣直接使用 int 常量無法限定使用者的輸入,你讓它輸 0 或 1,它偏偏輸個 45678。從程式碼可讀性來說,引數是個 int 值,並不是那麼直觀的就可以看出來應該輸入什麼。無奈之下,我只得掏出 《Java 程式設計思想》,來治治這兩個熊孩子。下面是我優化的程式:
public enum Child { DAVID, MARY }
public void order(Child who) {
switch (who) {
case DAVID:
System.out.println("大衛,去練武術!");
break;
case MARY:
System.out.println("瑪麗,去練跳舞!");
break;
default:
System.out.println("今天休息");
}
}
複製程式碼
實際上已經可以刪除 default
了,因為引數是列舉類 Child
,輸入範圍已經被限定為我定義的列舉,輸入其他值將無法通過編譯。兩個熊孩子終於可以愉快的去上課了。
列舉是 Java 1.5 中新增的引用型別,指由一組固定的常量組成合法值的型別,其實上面的例子並不那麼適合列舉,例如一年四季,一週七天這樣的,更加適合列舉。相比使用 int 常量來定義,列舉具有型別安全和可讀性良好的優勢。《Effective Java》中也鼓勵 用 enum 代替 int 常量
。
除此之外,還可以給列舉新增欄位和方法,例如,我想給每個孩子加上姓名,可以這麼做:
public enum Child {
DAVID("David"), MARY("Marry");
private String name;
Child(String name){
this.name=name;
}
public static void main(String[] args){
for (Child child:Child.values()){
System.out.println(child.ordinal()+" "+child.name);
}
}
}
複製程式碼
注意,列舉的定義只能放在第一行,如果你把 DAVID("David"), MARY("Marry");
放在其他位置,是無法通過編譯的。另外別忘了,最後一個列舉常量後面要加分號。
values()
會列舉出 Child 中定義的所有列舉常量。列印結果如下:
0 David
1 Marry
複製程式碼
是不是比之前的 int 常量那種方式強大多了。
原始碼解析
Enum
走進 JDK 系列,那必然是少不了原始碼解析的。
類定義
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {}
複製程式碼
Enum
是一個抽象類,我們自己定義的列舉類都繼承了 Enum
。實現了 Comparable
介面,重寫了 compare
的邏輯。實現了 Serializable
介面,可以序列化。但是列舉對序列化作了一定的限制,在序列化的時候僅僅是將列舉物件的 name 屬性輸出到結果中,反序列化的時候則是通過 Enum.valueOf()
方法來查詢列舉物件。同時,編譯器是不允許任何對這種序列化機制的定製的,因此禁用了 writeObject
、readObject
、readObjectNoData
、writeReplace
和 readResolve
等方法。因此,用列舉來實現單例模式的話,是反序列化安全的,因為即使反序列化也不會生成新的物件。
欄位
private final String name; // 列舉例項的名稱,就是列舉宣告中的名稱
private final int ordinal; // 在列舉宣告中的次序,從 0 開始
複製程式碼
列舉類就只有這兩個欄位。name
就是列舉宣告的名字,比如 DAVID
的 name
就是 DAVID
。ordinal
就是宣告中的次序,之所以在 switch
中可以使用列舉,就是因為編譯器會自動呼叫列舉的 ordinal()
方法。
建構函式
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
複製程式碼
受保護的,你不能呼叫列舉類的建構函式。
方法
public final String name() {
return name; // 返回 name
}
public final int ordinal() {
return ordinal; // 返回 ordinal
}
public String toString() {
return name; // 可以重寫使得列舉類返回一個使用者友好的名字,預設返回 name
}
public final boolean equals(Object other) {
return this==other; // 列舉的 equals 和 == 是等價的
}
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException(); // 列舉不支援 clone,單例
}
public final int compareTo(E o) {
Enum<?> other = (Enum<?>)o;
Enum<E> self = this;
if (self.getClass() != other.getClass() && // optimization
self.getDeclaringClass() != other.getDeclaringClass())
throw new ClassCastException();
return self.ordinal - other.ordinal; // 實際上是比較 ordinal
}
// 根據指定的列舉型別和名稱返回列舉例項,在反序列化中會使用
public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
複製程式碼
還記得之前使用過的 values()
方法嗎,用來遍歷列舉的。找遍了 Enum.java
也沒有看到這個方法,既然父類中沒有這個方法,那麼一定是在子類中宣告的了。下面我們來驗證一下。
Enum 子類
我們是怎麼定義列舉的,
public enum Child {
DAVID("David"), MARY("Marry");
private String name;
Child(String name){
this.name=name;
}
}
複製程式碼
並沒有顯示的去繼承 Enum
,而是使用了 enum
關鍵字,雖然沒有使用 class
關鍵字,但其實它還是一個類,只是編譯器幫我們做了中間步驟。之前有說過,知其然不知其所以然的時候,javap
是你的好幫手。這次我們不 javap 了,畢竟位元組碼可讀性不是那麼的好。我們使用 jad
反編譯 Child.class
檔案,得到結果如下:
// Decompiled by Jad v1.5.8e. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.geocities.com/kpdus/jad.html
// Decompiler options: packimports(3)
// Source File Name: Child.java
package enums;
public final class Child extends Enum {
public static Child[] values() {
return (Child[])$VALUES.clone();
}
public static Child valueOf(String s) {
return (Child)Enum.valueOf(enums/Child, s);
}
private Child(String s, int i, String s1) {
super(s, i);
name = s1;
}
public static final Child DAVID;
public static final Child MARY;
private String name;
private static final Child $VALUES[];
static {
DAVID = new Child("DAVID", 0, "David");
MARY = new Child("MARY", 1, "Marry");
$VALUES = (new Child[] {
DAVID, MARY
});
}
}
複製程式碼
看到這個東西,你就應該全明白了。說起來叫列舉,其實就是普通的類,只是我們只需要使用 enum
關鍵字,編譯器就會幫我們做好一切。列舉中宣告的變數都是 static final
的,且在 static 程式碼塊中進行初始化,並存入物件陣列 $VALUES
。所以列舉例項的建立預設是執行緒安全的。除此之外,編譯器還自動生成了 values()
方法,返回 $VALUES
陣列的克隆。
說到這裡,你應該對列舉的原理很清楚了。列舉的種種特性都特別契合單例模式,天生的執行緒安全和反序列化安全,這都是其他單例模式所不具備的。但是在我所見過的程式碼中,真正使用列舉去做單例的好像少之又少。具體的原因有待考究。
真的要使用列舉嗎?
站在 Android 開發者的角度,實際上官方是不建議我們使用列舉的。
列舉佔用的空間通常是靜態常量的兩倍。你應該嚴格避免在 Android 中使用列舉。
其實我並不是完全贊同。MVP
多了那麼多介面和類,我們應該使用嗎?在如今的手機記憶體下,如果你的應用發生了 OOM
,我想列舉應該不是罪魁禍首吧。只要不過度使用,站在增強程式碼可讀性,保證型別安全的角度,用列舉代替靜態常量肯定是個好選擇。當然如果你說你要追求極致的效能,那倒不必使用列舉了。
文章首發微信公眾號:
秉心說
, 專注 Java 、 Android 原創知識分享,LeetCode 題解,歡迎關注!