溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

再談在Java中使用枚舉(轉(zhuǎn))

發(fā)布時間:2020-08-11 08:53:09 來源:ITPUB博客 閱讀:134 作者:BSDLite 欄目:編程語言
再談在Java中使用枚舉(轉(zhuǎn))[@more@]從C++轉(zhuǎn)到Java上的程序員一開始總是對Java有不少抱怨,其中沒有枚舉就是一個比較突出的問題。那么為什么Java不支持枚舉呢?從程序語言的角度講,支持枚舉意味著什么呢?我們能不能找到一種方法滿足C++程序員對枚舉的要求呢?那么現(xiàn)在就讓我們一起來探討一下這個問題。

枚舉類型(Enumerated Types)

讓我們先看下面這一段小程序:

enum Day {SUNDAY, MONDAY, TUESDAY,
WEDNESDAY, THURSDAY, FRIDAY, SATURDAY};



這種申明提供了一種用戶友好的變量定義的方法,它枚舉了這種數(shù)據(jù)類型所有可能的值,即星期一到星期天。拋開具體編程語言來看,枚舉所具有的核心功能應(yīng)該是:


類型安全(Type Safety)


緊湊有效的枚舉數(shù)值定義 (Compact, Efficient Declaration of Enumerated Values)


無縫的和程序其它部分的交互操作(Seamless integration with other language features)


運(yùn)行的高效率(Runtime efficiency)

現(xiàn)在我們就這幾個特點(diǎn)逐一討論一下。

1. 類型安全

枚舉的申明創(chuàng)建了一個新的類型。它不同于其他的已有類型,包括原始類型(整數(shù),浮點(diǎn)數(shù)等等)和當(dāng)前作用域(Scope)內(nèi)的其它的枚舉類型。當(dāng)你對函數(shù)的參數(shù)進(jìn)行賦值操作的時候,整數(shù)類型和枚舉類型是不能互換的(除非是你進(jìn)行顯式的類型轉(zhuǎn)換),編譯器將強(qiáng)制這一點(diǎn)。比如說,用上面申明的枚舉定義這樣一個函數(shù):

public void foo(Day);



如果你用整數(shù)來調(diào)用這個函數(shù),編譯器會給出錯誤的。

foo(4); // compilation error



如果按照這個標(biāo)準(zhǔn),那么Pascal, Ada, 和C++是嚴(yán)格意義上的支持枚舉,而C語言都不是。

2. 緊湊有效的枚舉數(shù)值定義

定義枚巨的程序應(yīng)該很簡單。比如說,在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;



這種定義就似乎不夠簡潔。如果有大量的數(shù)據(jù)要定義,這一點(diǎn)就尤為重要,你也就會感受更深。雖然這一點(diǎn)不如其他另外3點(diǎn)重要,但我們總是希望申明能盡可能的簡潔。

3. 無縫的和程序其它部分的交互操作

語言的運(yùn)算符,如賦值,相等/大于/小于判斷都應(yīng)該支持枚舉。枚舉還應(yīng)該支持?jǐn)?shù)組下標(biāo)以及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: ...;
}
}



要想讓這段程序工作,那么枚舉必須是整數(shù)常數(shù),而不能是對象(objects)。Java中你可以用equals() 或是 compareTo() 函數(shù)來進(jìn)行對象的比較操作,但是它們都不支持?jǐn)?shù)組下標(biāo)和switch語句。

4. 運(yùn)行的高效率

枚舉的運(yùn)行效率應(yīng)該和原始類型的整數(shù)一樣高。在運(yùn)行時不應(yīng)該由于使用了枚舉而導(dǎo)致性能比使用整數(shù)有下降。

如果一種語言滿足這四點(diǎn)要求,那么我們可以說這種語言是真正的支持枚舉。比如前面所說的Pascal, Ada, 和C++。很明顯,Java不是。

Java的創(chuàng)始人James Gosling是個資深的C++程序員,他很清楚什么是枚舉。但似乎他有意的刪除了Java的枚舉能力。其原因我們不得而知??赡苁撬霃?qiáng)調(diào)和鼓勵使用多態(tài)性(polymorphism),不鼓勵使用多重分支。而多重分支往往是和枚舉聯(lián)合使用的。不管他的初衷如何,我們在Java中仍然需要枚舉。

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ú)放在一個類中。如果你選擇將其單獨(dú)放在一個類中,那么引用的時候要注意語法。比如"Day.MONDAY."。如果你想在引用的時候省一點(diǎn)事,那么你可以將其放在一個接口中(interface),其它類只要申明實(shí)現(xiàn)(implement)它就可以比較方便的引用。比如直接使用MONDAY。就Java接口的使用目的而言,這種用法有些偏,不用也罷!

這種方法顯然滿足了條件3和4,即語言的集成和執(zhí)行效率(枚舉就是整數(shù),沒有效率損失)。但是他卻不能滿足條件1和2。它的定義有些啰嗦,更重要的是它不是類型安全的。這種方法雖然普遍被Java程序員采用,但它不是一種枚舉的良好替代品。

第二種方法是被一些有名的專家經(jīng)常提及的。我們可以稱它為"對象枚舉"。即為枚舉創(chuàng)建一個類,然后用公用的該類的對象來表達(dá)每一個枚舉的值。如下所示:

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)建一個新的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)對象(public static object)。此外該類含有私有構(gòu)造函數(shù);一個循環(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)檫@個而不去使用它;同時他還不可以被用作數(shù)組下標(biāo)或是用在switch/case語句。這在一定程度上降低了他的使用的廣泛性。

看起來,沒有一種替代品是理想的。我們雖然沒有權(quán)利修改Java語言,但是我們也許可以想一些辦法來克服"對象枚舉"的缺點(diǎn),使它成為合格的枚舉替代品。
一個實(shí)現(xiàn)枚舉的微型語言(AMini-Language for Enums)

假如我發(fā)明一種枚舉專用的微型語言(且叫它jEnum),它專門用來申明枚舉。然后我再用一個特殊的"翻譯"程序?qū)⑽矣眠@種語言定義的枚舉轉(zhuǎn)化為對應(yīng)的"對象枚舉"定義,那不是就解決了"對象枚舉"定義復(fù)雜的問題了嗎。當(dāng)然我們很容易讓這個"翻譯"程序多做一些工作。比如加入Package申明,加入程序注釋,說明整數(shù)值和該對象的字符串標(biāo)簽名稱等等。讓我們看下面這樣一個例子:

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)比較顯著。但是總體上講"對象枚舉"提供的類型安全還是更為重要的,相比之下哪些缺點(diǎn)還是比較次要的。下面我們大概講一下jEnum,使用它我們又可以得到緊湊和有效的枚舉申明這一特點(diǎn),也就是我們前面提到的條件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的注解,當(dāng)然這不是必須的。枚舉類型的申明以關(guān)鍵字"enum"開頭,枚舉的值放在花括號中{},多個值之間用逗號分開。每一個值的申明包括一個標(biāo)準(zhǔn)的Java變量名,一個可選的字符串標(biāo)簽,可選的等號(=)和一個整數(shù)值。

如果你省略了字符串標(biāo)簽,那么枚舉的變量名就會被使用;如果你省略了等號和后面的整數(shù)值,那么它將會自動按順序給你的枚舉賦值,如果沒有使用任何數(shù)值,那么它從零開始逐步增加(步長為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)格向上增加的,象下面這段申明就是不對的,因?yàn)樗淖址畼?biāo)簽不是唯一的。

enum Color { RED("Red"), WHITE("BLUE"), BLUE };



下面這段申明也是不對的,因?yàn)閃HITE會被自動賦值2 ,和BLUE有沖突。

enum Color { RED = 1, WHITE, BLUE = 2 };



下面這是一個具體的實(shí)例。它將會被"翻譯"程序使用,用以轉(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
};



如果對Day的枚舉申明存放在Day.enum文件中,那么我們可以將這個文件翻譯成Java源程序。

$ java -jar jEnum.jar Day.enum



翻譯的結(jié)果就是Day.javaJava源程序,內(nèi)容和我們前面講的一樣,還包括程序注釋等內(nèi)容。如果想省一點(diǎn)事,你可以將上面比較長的命令寫成一個批處理文件或是Unix,Linux上的shell script,那么以后使用的時候就可以簡單一些,比如:

$ jec Day.enum



關(guān)于jEnum有四點(diǎn)注意事項(xiàng)要說明一下。

1. 申明文件名不一定后綴為".enum.",其它合法文件后綴都可以。

2. 如果文件后綴不是".enum.",那么翻譯程序?qū)⑹紫劝唇o出的文件名去搜索,如果沒有,就假定給出的文件名是省略了".enum."后綴的。像這種命令是可以的:

$ java -jar jEnum.jar Day



3. 生成的Java源程序文件名是按照申明文件內(nèi)的定義得出的,而不是依據(jù)申明文件的名稱。

4. 翻譯程序還接受以下幾個開關(guān)

-o 生成"對象枚舉"類枚舉,是缺省值

-c 生成"整數(shù)常數(shù)枚舉"類枚舉,用類來實(shí)現(xiàn)

-i 生成"整數(shù)常數(shù)枚舉"類枚舉,用接口來實(shí)現(xiàn)

要注意的是,-C開關(guān)雖然生成"整數(shù)常數(shù)枚舉",但它同時還提供了一些"對象枚舉"中所具有的方法,如first(), last(),toString(int n),prev(int n), 和next(int n)。
向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI