再談在Java中使用枚舉

來源:互聯網
上載者:User

從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)。

jEnum工具可以從網上自由下載,其地址是:http://www.onjava.com/onjava/2003/04/23/examples/jEnum.zip

 

http://www.cn-java.com/www1/?action-viewnews-itemid-2919

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.