再談在Java中使用列舉(轉)

BSDLite發表於2007-08-15
再談在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)。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617542/viewspace-960316/,如需轉載,請註明出處,否則將追究法律責任。

相關文章