再談在Java中使用枚舉(轉)
再談在Java中使用枚舉(轉)[@more@]從C++轉到Java上的程序員一開始總是對Java有不少抱怨,其中沒有枚舉就是一個比較突出的問題。那么為什么Java不支持枚舉呢?從程序語言的角度講,支持枚舉意味著什么呢?我們能不能找到一種方法滿足C++程序員對枚舉的要求呢?那么現在就讓我們一起來探討一下這個問題。
枚舉類型(Enumerated Types)
讓我們先看下面這一段小程序:
enum Day {SUNDAY, MONDAY, TUESDAY,
WEDNESDAY, THURSDAY, FRIDAY, SATURDAY};
這種申明提供了一種用戶友好的變量定義的方法,它枚舉了這種數據類型所有可能的值,即星期一到星期天。拋開具體編程語言來看,枚舉所具有的核心功能應該是:
類型安全(Type Safety)
緊湊有效的枚舉數值定義 (Compact, Efficient Declaration of Enumerated Values)
無縫的和程序其它部分的交互操作(Seamless integration with other language features)
運行的高效率(Runtime efficiency)
現在我們就這幾個特點逐一討論一下。
1. 類型安全
枚舉的申明創建了一個新的類型。它不同于其他的已有類型,包括原始類型(整數,浮點數等等)和當前作用域(Scope)內的其它的枚舉類型。當你對函數的參數進行賦值操作的時候,整數類型和枚舉類型是不能互換的(除非是你進行顯式的類型轉換),編譯器將強制這一點。比如說,用上面申明的枚舉定義這樣一個函數:
public void foo(Day);
如果你用整數來調用這個函數,編譯器會給出錯誤的。
foo(4); // compilation error
如果按照這個標準,那么Pascal, Ada, 和C++是嚴格意義上的支持枚舉,而C語言都不是。
2. 緊湊有效的枚舉數值定義
定義枚巨的程序應該很簡單。比如說,在Java中我們有這樣一種"準枚舉"的定義方法:
public static final int SUNDAY = 0;
public static final int MONDAY = 1;
public static final int TUESDAY = 2;
public static final int WEDNESDAY = 3;
public static final int THURSDAY = 4;
public static final int FRIDAY = 5;
public static final int SATURDAY = 6;
這種定義就似乎不夠簡潔。如果有大量的數據要定義,這一點就尤為重要,你也就會感受更深。雖然這一點不如其他另外3點重要,但我們總是希望申明能盡可能的簡潔。
3. 無縫的和程序其它部分的交互操作
語言的運算符,如賦值,相等/大于/小于判斷都應該支持枚舉。枚舉還應該支持數組下標以及switch/case語句中用來控制流程的操作。比如:
for (Day d = SUNDAY; d <= SATURDAY; ++d) {
switch(d) {
case MONDAY: ...;
break;
case TUESDAY: ...;
break;
case WEDNESDAY: ...;
break;
case THURSDAY: ...;
break;
case FRIDAY: ...;
break;
case SATURDAY:
case SUNDAY: ...;
}
}
要想讓這段程序工作,那么枚舉必須是整數常數,而不能是對象(objects)。Java中你可以用equals() 或是 compareTo() 函數來進行對象的比較操作,但是它們都不支持數組下標和switch語句。
4. 運行的高效率
枚舉的運行效率應該和原始類型的整數一樣高。在運行時不應該由于使用了枚舉而導致性能比使用整數有下降。
如果一種語言滿足這四點要求,那么我們可以說這種語言是真正的支持枚舉。比如前面所說的Pascal, Ada, 和C++。很明顯,Java不是。
Java的創始人James Gosling是個資深的C++程序員,他很清楚什么是枚舉。但似乎他有意的刪除了Java的枚舉能力。其原因我們不得而知。可能是他想強調和鼓勵使用多態性(polymorphism),不鼓勵使用多重分支。而多重分支往往是和枚舉聯合使用的。不管他的初衷如何,我們在Java中仍然需要枚舉。
Java中的幾種"準枚舉"類型
雖然Java 不直接支持用戶定義的枚舉。但是在實踐中人們還是總結出一些枚舉的替代品。
第一種替代品可以解釋為"整數常數枚舉"。如下所示:
public static final int SUNDAY = 0;
public static final int MONDAY = 1;
public static final int TUESDAY = 2;
public static final int WEDNESDAY = 3;
public static final int THURSDAY = 4;
public static final int FRIDAY = 5;
public static final int SATURDAY = 6;
這種方法可以讓我們使用更有意義的變量名而不是直接赤裸裸的整數值。這樣使得源程序的可讀性和可維護性更好一些。這些定義可以放在任何類中。可以和其它的變量和方法混在一起。也可以單獨放在一個類中。如果你選擇將其單獨放在一個類中,那么引用的時候要注意語法。比如"Day.MONDAY."。如果你想在引用的時候省一點事,那么你可以將其放在一個接口中(interface),其它類只要申明實現(implement)它就可以比較方便的引用。比如直接使用MONDAY。就Java接口的使用目的而言,這種用法有些偏,不用也罷!
這種方法顯然滿足了條件3和4,即語言的集成和執行效率(枚舉就是整數,沒有效率損失)。但是他卻不能滿足條件1和2。它的定義有些啰嗦,更重要的是它不是類型安全的。這種方法雖然普遍被Java程序員采用,但它不是一種枚舉的良好替代品。
第二種方法是被一些有名的專家經常提及的。我們可以稱它為"對象枚舉"。即為枚舉創建一個類,然后用公用的該類的對象來表達每一個枚舉的值。如下所示:
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.io.Serializable;
import java.io.InvalidObjectException;
public final class Day implements Comparable, Serializable {
private static int size = 0;
private static int nextOrd = 0;
private static Map nameMap = new HashMap(10);
private static Day first = null;
private static Day last = null;
private final int ord;
private final String label;
private Day prev;
private Day next;
public static final Day SUNDAY = new Day("SUNDAY");
public static final Day MONDAY = new Day("MONDAY");
public static final Day TUESDAY = new Day("TUESDAY");
public static final Day WEDNESDAY = new Day("WEDNESDAY");
public static final Day THURSDAY = new Day("THURSDAY");
public static final Day FRIDAY = new Day("FRIDAY");
public static final Day SATURDAY = new Day("SATURDAY");
/**
* 用所給的標簽創建一個新的day.
* (Uses default value for ord.)
*/
private Day(String label) {
this(label, nextOrd);
}
/**
* Constructs a new Day with its label and ord value.
*/
private Day(String label, int ord) {
this.label = label;
this.ord = ord;
++size;
nextOrd = ord + 1;
nameMap.put(label, this);
if (first == null)
first = this;
if (last != null) {
this.prev = last;
last.next = this;
}
last = this;
}
/**
* Compares two Day objects based on their ordinal values.
* Satisfies requirements of interface java.lang.Comparable.
*/
public int compareTo(Object obj) {
return ord - ((Day)obj).ord;
}
/**
* Compares two Day objects for equality. Returns true
* only if the specified Day is equal to this one.
*/
public boolean equals(Object obj) {
return super.equals(obj);
}
/**
* Returns a hash code value for this Day.
*/
public int hashCode() {
return super.hashCode();
}
/**
* Resolves deserialized Day objects.
* @throws InvalidObjectException if deserialization fails.
*/
private Object readResolve() throws InvalidObjectException {
Day d = get(label);
if (d != null)
return d;
else {
String msg = "invalid deserialized object: label = ";
throw new InvalidObjectException(msg + label);
}
}
/**
* Returns Day with the specified label.
* Returns null if not found.
*/
public static Day get(String label) {
return (Day) nameMap.get(label);
}
/**
* Returns the label for this Day.
*/
public String toString() {
return label;
}
/**
* Always throws CloneNotSupportedException; guarantees that
* Day objects are never cloned.
*
* @return (never returns)
*/
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
/**
* Returns an iterator over all Day objects in declared order.
*/
public static Iterator iterator() {
// anonymous inner class
return new Iterator()
{
private Day current = first;
public boolean hasNext() {
return current != null;
}
public Object next() {
Day d = current;
current = current.next();
return d;
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
/**
* Returns the ordinal value of this Day.
*/
public int ord() {
return this.ord;
}
/**
* Returns the number of declared Day objects.
*/
public static int size() {
return size;
}
/**
* Returns the first declared Day.
*/
public static Day first() {
return first;
}
/**
* Returns the last declared Day.
*/
public static Day last() {
return last;
}
/**
* Returns the previous Day before this one in declared order.
* Returns null for the first declared Day.
*/
public Day prev() {
return this.prev;
}
/**
* Returns the next Day after this one in declared order.
* Returns null for the last declared Day.
*/
public Day next() {
return this.next;
}
}
枚舉值被定義為公用靜態對象(public static object)。此外該類含有私有構造函數;一個循環器(Iterator)用以遍歷所有的值;一些Java中常用的函數,如toString(),equals()和compareTo(),以及一些方便客戶程序調用的函數,如ord(),prev(),next(),first()和 last()。
這種實現方法有很好的類型安全和運行效率(條件1和4)。但是去不滿足條件2和3。首先它的定義比較繁瑣,大多數程序員也許因為這個而不去使用它;同時他還不可以被用作數組下標或是用在switch/case語句。這在一定程度上降低了他的使用的廣泛性。
看起來,沒有一種替代品是理想的。我們雖然沒有權利修改Java語言,但是我們也許可以想一些辦法來克服"對象枚舉"的缺點,使它成為合格的枚舉替代品。
一個實現枚舉的微型語言(AMini-Language for Enums)
假如我發明一種枚舉專用的微型語言(且叫它jEnum),它專門用來申明枚舉。然后我再用一個特殊的"翻譯"程序將我用這種語言定義的枚舉轉化為對應的"對象枚舉"定義,那不是就解決了"對象枚舉"定義復雜的問題了嗎。當然我們很容易讓這個"翻譯"程序多做一些工作。比如加入Package申明,加入程序注釋,說明整數值和該對象的字符串標簽名稱等等。讓我們看下面這樣一個例子:
package com.softmoore.util;
/**
* Various USA coins
*/
enum Coin { PENNY("penny") = 1, NICKEL("nickel") = 5, DIME("dime") = 10,
QUARTER("quarter") = 25, HALF_DOLLAR("half dollar") = 50 };
雖然"整數常數枚舉"在有些情況下優點比較顯著。但是總體上講"對象枚舉"提供的類型安全還是更為重要的,相比之下哪些缺點還是比較次要的。下面我們大概講一下jEnum,使用它我們又可以得到緊湊和有效的枚舉申明這一特點,也就是我們前面提到的條件2。
熟悉編譯器的朋友可能更容易理解下面這一段jEnum微型語言。
compilationUnit = ( packageDecl )? ( docComment )? enumTypeDecl .
packageDecl = "package" packagePath ";" .
packagePath = packageName ( "." packageName )* .
docComment = "/**" commentChars "*/" .
enumTypeDecl = "enum" enumTypeName "{" enumList "}" ";" .
enumList = enumDecl ( "," enumDecl )* .
enumDecl = enumLiteral ( "(" stringLiteral ")" )? ( "=" intLiteral )? .
packageName = identifier .
enumTypeName = identifier .
enumLiteral = identifier .
commentChars = any-char-sequence-except-"*/"
這種語法允許在開始申明package,看起來和Java語言還挺像。你可以增加一些javadoc的注解,當然這不是必須的。枚舉類型的申明以關鍵字"enum"開頭,枚舉的值放在花括號中{},多個值之間用逗號分開。每一個值的申明包括一個標準的Java變量名,一個可選的字符串標簽,可選的等號(=)和一個整數值。
如果你省略了字符串標簽,那么枚舉的變量名就會被使用;如果你省略了等號和后面的整數值,那么它將會自動按順序給你的枚舉賦值,如果沒有使用任何數值,那么它從零開始逐步增加(步長為1)。字符串標簽作為toString()方法返回值的一部分,而整數值則作為ord()方法的返回值。如下面這段申明:
enum Color { RED("Red") = 2, WHITE("White") = 4, BLUE };
RED 的標簽是 "Red",值為 2 ;
WHITE的標簽是"White",值為4;
BLUE的標簽是"BLUE" ,值為5 。
要注意的是在Java中的保留字在jEnum也是保留的。比如你不可以使用this作為package名,不可以用for為枚舉的變量名等等。枚舉的變量名和字符串標簽必須是不同的,其整數值也必須是嚴格向上增加的,象下面這段申明就是不對的,因為它的字符串標簽不是唯一的。
enum Color { RED("Red"), WHITE("BLUE"), BLUE };
下面這段申明也是不對的,因為WHITE會被自動賦值2 ,和BLUE有沖突。
enum Color { RED = 1, WHITE, BLUE = 2 };
下面這是一個具體的實例。它將會被"翻譯"程序使用,用以轉換成我們枚舉申明為可編譯的Java源程序。
package com.softmoore.jEnum;
/**
* This class encapsulates the symbols (a.k.a. token types)
* of a language token.
*/
enum Symbol {
identifier,
enumRW("Reserved Word: enum"),
abstractRW("Reserved Word: abstract"),
assertRW("Reserved Word: assert"),
booleanRW("Reserved Word: boolean"),
breakRW("Reserved Word: break"),
byteRW("Reserved Word: byte"),
caseRW("Reserved Word: case"),
catchRW("Reserved Word: catch"),
charRW("Reserved Word: char"),
classRW("Reserved Word: class"),
constRW("Reserved Word: const"),
continueRW("Reserved Word: continue"),
defaultRW("Reserved Word: default"),
doRW("Reserved Word: do"),
doubleRW("Reserved Word: double"),
elseRW("Reserved Word: else"),
extendsRW("Reserved Word: extends"),
finalRW("Reserved Word: final"),
finallyRW("Reserved Word: finally"),
floatRW("Reserved Word: float"),
forRW("Reserved Word: for"),
gotoRW("Reserved Word: goto"),
ifRW("Reserved Word: if"),
implementsRW("Reserved Word: implements"),
importRW("Reserved Word: import"),
instanceOfRW("Reserved Word: instanceOf"),
intRW("Reserved Word: int"),
interfaceRW("Reserved Word: interface"),
longRW("Reserved Word: long"),
nativeRW("Reserved Word: native"),
newRW("Reserved Word: new"),
nullRW("Reserved Word: null"),
packageRW("Reserved Word: package"),
privateRW("Reserved Word: private"),
protectedRW("Reserved Word: protected"),
publicRW("Reserved Word: public"),
returnRW("Reserved Word: return"),
shortRW("Reserved Word: short"),
staticRW("Reserved Word: static"),
strictfpRW("Reserved Word: strictfp"),
superRW("Reserved Word: super"),
switchRW("Reserved Word: switch"),
synchronizedRW("Reserved Word: synchronized"),
thisRW("Reserved Word: this"),
throwRW("Reserved Word: throw"),
throwsRW("Reserved Word: throws"),
transientRW("Reserved Word: transient"),
tryRW("Reserved Word: try"),
voidRW("Reserved Word: void"),
volatileRW("Reserved Word: volatile"),
whileRW("Reserved Word: while"),
equals("="),
leftParen("("),
rightParen(")"),
leftBrace("{"),
rightBrace("}"),
comma(","),
semicolon(";"),
period("."),
intLiteral,
stringLiteral,
docComment,
EOF,
unknown
};
如果對Day的枚舉申明存放在Day.enum文件中,那么我們可以將這個文件翻譯成Java源程序。
$ java -jar jEnum.jar Day.enum
翻譯的結果就是Day.javaJava源程序,內容和我們前面講的一樣,還包括程序注釋等內容。如果想省一點事,你可以將上面比較長的命令寫成一個批處理文件或是Unix,Linux上的shell script,那么以后使用的時候就可以簡單一些,比如:
$ jec Day.enum
關于jEnum有四點注意事項要說明一下。
1. 申明文件名不一定后綴為".enum.",其它合法文件后綴都可以。
2. 如果文件后綴不是".enum.",那么翻譯程序將首先按給出的文件名去搜索,如果沒有,就假定給出的文件名是省略了".enum."后綴的。像這種命令是可以的:
$ java -jar jEnum.jar Day
3. 生成的Java源程序文件名是按照申明文件內的定義得出的,而不是依據申明文件的名稱。
4. 翻譯程序還接受以下幾個開關
-o 生成"對象枚舉"類枚舉,是缺省值
-c 生成"整數常數枚舉"類枚舉,用類來實現
-i 生成"整數常數枚舉"類枚舉,用接口來實現
要注意的是,-C開關雖然生成"整數常數枚舉",但它同時還提供了一些"對象枚舉"中所具有的方法,如first(), last(),toString(int n),prev(int n), 和next(int n)。