從C++轉(zhuǎn)到Java上的程序員一開始總是對(duì)Java有不少抱怨,其中沒有枚舉就是一個(gè)比較突出的問題。那么為什么Java不支持枚舉呢?從程序語(yǔ)言的角度講,支持枚舉意味著什么呢?我們能不能找到一種方法滿足C++程序員對(duì)枚舉的要求呢?那么現(xiàn)在就讓我們一起來(lái)探討一下這個(gè)問題。
枚舉類型(Enumerated Types)
讓我們先看下面這一段小程序:
enum Day {SUNDAY, MONDAY, TUESDAY,
WEDNESDAY, THURSDAY, FRIDAY, SATURDAY};
這種申明提供了一種用戶友好的變量定義的方法,它枚舉了這種數(shù)據(jù)類型所有可能的值,即星期一到星期天。拋開具體編程語(yǔ)言來(lái)看,枚舉所具有的核心功能應(yīng)該是:
類型安全(Type Safety)
緊湊有效的枚舉數(shù)值定義 (Compact, Efficient Declaration of Enumerated Values)
無(wú)縫的和程序其它部分的交互操作(Seamless integration with other language features)
運(yùn)行的高效率(Runtime efficiency)
現(xiàn)在我們就這幾個(gè)特點(diǎn)逐一討論一下。
1. 類型安全
枚舉的申明創(chuàng)建了一個(gè)新的類型。它不同于其他的已有類型,包括原始類型(整數(shù),浮點(diǎn)數(shù)等等)和當(dāng)前作用域(Scope)內(nèi)的其它的枚舉類型。當(dāng)你對(duì)函數(shù)的參數(shù)進(jìn)行賦值操作的時(shí)候,整數(shù)類型和枚舉類型是不能互換的(除非是你進(jìn)行顯式的類型轉(zhuǎn)換),編譯器將強(qiáng)制這一點(diǎn)。比如說(shuō),用上面申明的枚舉定義這樣一個(gè)函數(shù):
public void foo(Day);
如果你用整數(shù)來(lái)調(diào)用這個(gè)函數(shù),編譯器會(huì)給出錯(cuò)誤的。
foo(4); // compilation error
如果按照這個(gè)標(biāo)準(zhǔn),那么Pascal, Ada, 和C++是嚴(yán)格意義上的支持枚舉,而C語(yǔ)言都不是。
2. 緊湊有效的枚舉數(shù)值定義
定義枚巨的程序應(yīng)該很簡(jiǎn)單。比如說(shuō),在Java中我們有這樣一種"準(zhǔn)枚舉"的定義方法:
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;
這種定義就似乎不夠簡(jiǎn)潔。如果有大量的數(shù)據(jù)要定義,這一點(diǎn)就尤為重要,你也就會(huì)感受更深。雖然這一點(diǎn)不如其他另外3點(diǎn)重要,但我們總是希望申明能盡可能的簡(jiǎn)潔。
3. 無(wú)縫的和程序其它部分的交互操作
語(yǔ)言的運(yùn)算符,如賦值,相等/大于/小于判斷都應(yīng)該支持枚舉。枚舉還應(yīng)該支持?jǐn)?shù)組下標(biāo)以及switch/case語(yǔ)句中用來(lái)控制流程的操作。比如:
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: ...;
}
}
要想讓這段程序工作,那么枚舉必須是整數(shù)常數(shù),而不能是對(duì)象(objects)。Java中你可以用equals() 或是 compareTo() 函數(shù)來(lái)進(jìn)行對(duì)象的比較操作,但是它們都不支持?jǐn)?shù)組下標(biāo)和switch語(yǔ)句。
4. 運(yùn)行的高效率
枚舉的運(yùn)行效率應(yīng)該和原始類型的整數(shù)一樣高。在運(yùn)行時(shí)不應(yīng)該由于使用了枚舉而導(dǎo)致性能比使用整數(shù)有下降。
如果一種語(yǔ)言滿足這四點(diǎn)要求,那么我們可以說(shuō)這種語(yǔ)言是真正的支持枚舉。比如前面所說(shuō)的Pascal, Ada, 和C++。很明顯,Java不是。
Java的創(chuàng)始人James Gosling是個(gè)資深的C++程序員,他很清楚什么是枚舉。但似乎他有意的刪除了Java的枚舉能力。其原因我們不得而知。可能是他想強(qiáng)調(diào)和鼓勵(lì)使用多態(tài)性(polymorphism),不鼓勵(lì)使用多重分支。而多重分支往往是和枚舉聯(lián)合使用的。不管他的初衷如何,我們?cè)贘ava中仍然需要枚舉。
Java中的幾種"準(zhǔn)枚舉"類型
雖然Java 不直接支持用戶定義的枚舉。但是在實(shí)踐中人們還是總結(jié)出一些枚舉的替代品。
第一種替代品可以解釋為"整數(shù)常數(shù)枚舉"。如下所示:
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;
這種方法可以讓我們使用更有意義的變量名而不是直接赤裸裸的整數(shù)值。這樣使得源程序的可讀性和可維護(hù)性更好一些。這些定義可以放在任何類中。可以和其它的變量和方法混在一起。也可以單獨(dú)放在一個(gè)類中。如果你選擇將其單獨(dú)放在一個(gè)類中,那么引用的時(shí)候要注意語(yǔ)法。比如"Day.MONDAY."。如果你想在引用的時(shí)候省一點(diǎn)事,那么你可以將其放在一個(gè)接口中(interface),其它類只要申明實(shí)現(xiàn)(implement)它就可以比較方便的引用。比如直接使用MONDAY。就Java接口的使用目的而言,這種用法有些偏,不用也罷!
這種方法顯然滿足了條件3和4,即語(yǔ)言的集成和執(zhí)行效率(枚舉就是整數(shù),沒有效率損失)。但是他卻不能滿足條件1和2。它的定義有些啰嗦,更重要的是它不是類型安全的。這種方法雖然普遍被Java程序員采用,但它不是一種枚舉的良好替代品。
第二種方法是被一些有名的專家經(jīng)常提及的。我們可以稱它為"對(duì)象枚舉"。即為枚舉創(chuàng)建一個(gè)類,然后用公用的該類的對(duì)象來(lái)表達(dá)每一個(gè)枚舉的值。如下所示:
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");
/**
* 用所給的標(biāo)簽創(chuàng)建一個(gè)新的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;
}
}
枚舉值被定義為公用靜態(tài)對(duì)象(public static object)。此外該類含有私有構(gòu)造函數(shù);一個(gè)循環(huán)器(Iterator)用以遍歷所有的值;一些Java中常用的函數(shù),如toString(),equals()和compareTo(),以及一些方便客戶程序調(diào)用的函數(shù),如ord(),prev(),next(),first()和 last()。
這種實(shí)現(xiàn)方法有很好的類型安全和運(yùn)行效率(條件1和4)。但是去不滿足條件2和3。首先它的定義比較繁瑣,大多數(shù)程序員也許因?yàn)檫@個(gè)而不去使用它;同時(shí)他還不可以被用作數(shù)組下標(biāo)或是用在switch/case語(yǔ)句。這在一定程度上降低了他的使用的廣泛性。
看起來(lái),沒有一種替代品是理想的。我們雖然沒有權(quán)利修改Java語(yǔ)言,但是我們也許可以想一些辦法來(lái)克服"對(duì)象枚舉"的缺點(diǎn),使它成為合格的枚舉替代品。
一個(gè)實(shí)現(xiàn)枚舉的微型語(yǔ)言(AMini-Language for Enums)
假如我發(fā)明一種枚舉專用的微型語(yǔ)言(且叫它jEnum),它專門用來(lái)申明枚舉。然后我再用一個(gè)特殊的"翻譯"程序?qū)⑽矣眠@種語(yǔ)言定義的枚舉轉(zhuǎn)化為對(duì)應(yīng)的"對(duì)象枚舉"定義,那不是就解決了"對(duì)象枚舉"定義復(fù)雜的問題了嗎。當(dāng)然我們很容易讓這個(gè)"翻譯"程序多做一些工作。比如加入Package申明,加入程序注釋,說(shuō)明整數(shù)值和該對(duì)象的字符串標(biāo)簽名稱等等。讓我們看下面這樣一個(gè)例子:
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 };
雖然"整數(shù)常數(shù)枚舉"在有些情況下優(yōu)點(diǎn)比較顯著。但是總體上講"對(duì)象枚舉"提供的類型安全還是更為重要的,相比之下哪些缺點(diǎn)還是比較次要的。下面我們大概講一下jEnum,使用它我們又可以得到緊湊和有效的枚舉申明這一特點(diǎn),也就是我們前面提到的條件2。
熟悉編譯器的朋友可能更容易理解下面這一段jEnum微型語(yǔ)言。
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-"*/"
這種語(yǔ)法允許在開始申明package,看起來(lái)和Java語(yǔ)言還挺像。你可以增加一些javadoc的注解,當(dāng)然這不是必須的。枚舉類型的申明以關(guān)鍵字"enum"開頭,枚舉的值放在花括號(hào)中{},多個(gè)值之間用逗號(hào)分開。每一個(gè)值的申明包括一個(gè)標(biāo)準(zhǔn)的Java變量名,一個(gè)可選的字符串標(biāo)簽,可選的等號(hào)(=)和一個(gè)整數(shù)值。
如果你省略了字符串標(biāo)簽,那么枚舉的變量名就會(huì)被使用;如果你省略了等號(hào)和后面的整數(shù)值,那么它將會(huì)自動(dòng)按順序給你的枚舉賦值,如果沒有使用任何數(shù)值,那么它從零開始逐步增加(步長(zhǎng)為1)。字符串標(biāo)簽作為toString()方法返回值的一部分,而整數(shù)值則作為ord()方法的返回值。如下面這段申明:
enum Color { RED("Red") = 2, WHITE("White") = 4, BLUE };
RED 的標(biāo)簽是 "Red",值為 2 ;
WHITE的標(biāo)簽是"White",值為4;
BLUE的標(biāo)簽是"BLUE" ,值為5 。
要注意的是在Java中的保留字在jEnum也是保留的。比如你不可以使用this作為package名,不可以用for為枚舉的變量名等等。枚舉的變量名和字符串標(biāo)簽必須是不同的,其整數(shù)值也必須是嚴(yán)格向上增加的,象下面這段申明就是不對(duì)的,因?yàn)樗淖址畼?biāo)簽不是唯一的。
enum Color { RED("Red"), WHITE("BLUE"), BLUE };
下面這段申明也是不對(duì)的,因?yàn)閃HITE會(huì)被自動(dòng)賦值2 ,和BLUE有沖突。
enum Color { RED = 1, WHITE, BLUE = 2 };
下面這是一個(gè)具體的實(shí)例。它將會(huì)被"翻譯"程序使用,用以轉(zhuǎn)換成我們枚舉申明為可編譯的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
};
如果對(duì)Day的枚舉申明存放在Day.enum文件中,那么我們可以將這個(gè)文件翻譯成Java源程序。
$ java -jar jEnum.jar Day.enum
翻譯的結(jié)果就是Day.javaJava源程序,內(nèi)容和我們前面講的一樣,還包括程序注釋等內(nèi)容。如果想省一點(diǎn)事,你可以將上面比較長(zhǎng)的命令寫成一個(gè)批處理文件或是Unix,Linux上的shell script,那么以后使用的時(shí)候就可以簡(jiǎn)單一些,比如:
$ jec Day.enum
關(guān)于jEnum有四點(diǎn)注意事項(xiàng)要說(shuō)明一下。
1. 申明文件名不一定后綴為".enum.",其它合法文件后綴都可以。
2. 如果文件后綴不是".enum.",那么翻譯程序?qū)⑹紫劝唇o出的文件名去搜索,如果沒有,就假定給出的文件名是省略了".enum."后綴的。像這種命令是可以的:
$ java -jar jEnum.jar Day
3. 生成的Java源程序文件名是按照申明文件內(nèi)的定義得出的,而不是依據(jù)申明文件的名稱。
4. 翻譯程序還接受以下幾個(gè)開關(guān)
-o 生成"對(duì)象枚舉"類枚舉,是缺省值
-c 生成"整數(shù)常數(shù)枚舉"類枚舉,用類來(lái)實(shí)現(xiàn)
-i 生成"整數(shù)常數(shù)枚舉"類枚舉,用接口來(lái)實(shí)現(xiàn)
要注意的是,-C開關(guān)雖然生成"整數(shù)常數(shù)枚舉",但它同時(shí)還提供了一些"對(duì)象枚舉"中所具有的方法,如first(), last(),toString(int n),prev(int n), 和next(int n)。
jEnum工具可以從網(wǎng)上自由下載,其地址是:http://www.onjava.com/onjava/2003/04/23/examples/jEnum.zip
只有注冊(cè)用戶登錄后才能發(fā)表評(píng)論。 | ||
![]() |
||
網(wǎng)站導(dǎo)航:
博客園
IT新聞
Chat2DB
C++博客
博問
管理
|
||
相關(guān)文章:
|
||