1. 程式人生 > >java列舉和constant使用區別

java列舉和constant使用區別

本文結合《Effective Java》第六章前半部分關於列舉的介紹和自己的理解及實踐,講解了Java列舉的知識點。文章釋出於專欄Effective Java,歡迎讀者訂閱。

前言  你程式碼中的flag和status,都應該用列舉來替代
很多人都說,列舉在實際開發中很少用到,甚至就沒用到。因為,他們的程式碼往往是這樣子的:


public class Constant {
/*
* 以下幾個變量表示英雄的狀態
*/
public final static int STATUS_WALKING = 0;//走
public final static int STATUS_RUNNINGING = 1;//跑
public final static int STATUS_ATTACKING = 2;//攻擊
public final static int STATUS_DEFENDING = 3;//防禦
public final static int STATUS_DEAD = 4;//掛了

/*
* 以下幾個變量表示英雄的等級
*/
//此處略去N行程式碼
}

然後,他們是這樣使用這個類的:
hero.setStatus(Contant.STATUS_ATTACKING);

嗯,然後他們就說,“我在實際開發中很少用到列舉”

當然,他們的意思是說很少用到列舉Enum這個類。

但是,我想說的是,上面這些程式碼,通通應該用Enum去實現。

為什麼?

因為他們的程式碼完全建立在對隊友的信任,假設來了個奇葩隊友,做了這件事:

hero.setStatus(666);

你說,螢幕上的英雄會怎麼樣呢?

總之,假如你在實際程式設計中經常使用這樣的程式碼,那是時候好好學習一下Enum了。

 

列舉初探   為什麼要使用列舉型別
生活中處處都有列舉,包括“天然的列舉”,比如行星、一週的天數,也包括我們設計出來的列舉,比如csdn的tab標籤,選單等。

Java程式碼中表示列舉的方式,大體上有兩種,一是int列舉,而是Enum列舉,當然,我們都知道,Enum列舉才是Java提供的真正列舉。

那麼,為什麼我們要使用Enum列舉型別呢?先來看看在Java 1.5之前,沒有列舉型別時,我們是怎樣表示列舉的。

以八大行星為例,每個行星對應一個int值,我們大概會這樣寫


public class PlanetWithoutEnum {
public static final int PLANET_MERCURY = 0;
public static final int PLANET_VENUS = 1;
public static final int PLANET_EARTH = 2;
public static final int PLANET_MARS = 3;
public static final int PLANET_JUPITER = 4;
public static final int PLANET_SATURN = 5;
public static final int PLANET_URANUS = 6;
public static final int PLANET_NEPTUNE = 7;
}


這種叫int列舉模式,當然你也可以使用String列舉模式,無論採用何種方式,這樣的做法,在型別安全和使用方便性上都很差。

如果變數planet表示一個行星,使用者可以給這個值賦與一個不在我們列舉值裡面的值,比如 planet = 9,這是哪個行星估計也只有天知道了; 再者,我們很難計算出到底有多少個行星,我們也很難對行星進行遍歷操作等等。

現在我們用列舉來建立我們的行星。


public enum Planet {
MERCURY, VENUS, EARTH, MARS, JUPITER, SATURN, URANUS, NEPTUNE;
}


上面這個是最簡單的列舉,我們姑且叫做Planet 1.0,這個版本的行星列舉,我們實現了一個功能,就是任何一個Planet型別的變數,都可以由編譯器來保證,傳到給引數的任何非null物件一定屬於這八個行星之一。

然後,我們對Planet進行升級,Java允許我們給列舉型別新增任意的方法,這裡引言書中的程式碼,大家自行體會一下列舉的構造器、公共方法、列舉遍歷等知識點。


public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS(4.869e+24, 6.052e6),
EARTH(5.975e+24,6.378e6),
MARS(6.419e+23, 3.393e6),
JUPITER(1.899e+27, 7.149e7),
SATURN(5.685e+26, 6.027e7),
URANUS(8.683e+25, 2.556e7),
NEPTUNE(1.024e+26,2.477e7);
private final double mass; // In kilograms
private final double radius; // In meters
private final double surfaceGravity; // In m / s^2

// Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11;

// Constructor
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}

public double mass() {
return mass;
}

public double radius() {
return radius;
}

public double surfaceGravity() {
return surfaceGravity;
}

public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
}

//注:這裡對書中的程式碼做了微調
public class WeightTable {
public static void main(String[] args) {
printfWeightOnAllPlanets(8d);
}

public static void printfWeightOnAllPlanets(double earthWeight) {
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
}
}


執行WeightTable,列印結果如下:

Weight on MERCURY is 3.023254
Weight on VENUS is 7.240408
Weight on EARTH is 8.000000
Weight on MARS is 3.036832
Weight on JUPITER is 20.237436
Weight on SATURN is 8.524113
Weight on URANUS is 7.238844
Weight on NEPTUNE is 9.090108

在這個小程式裡,我們用到了列舉的values()方法,這個方法返回了列舉型別裡的列舉變數的集合,非常實用。

 

列舉進階   計算器運算子列舉類
上一小節的例子裡,我們用到了列舉類的公共方法,這一節,我們以計算器運算子 Operation 列舉類為例,看看怎麼實現對於每一個列舉物件,執行不同的操作。

首先,我們很容易想到的一個方法,在公共方法裡,使用switch去判斷列舉型別,然後執行不同的操作,程式碼如下:


public enum OperationUseSwitch {
PLUS, MINUS, TIMES, DIVIDE;

double apply(double x, double y) {
switch (this) {
case PLUS:
return x + y;
case MINUS:
return x + y;
case TIMES:
return x + y;
case DIVIDE:
return x + y;
}
// 如果this不屬於上面四種操作符,丟擲異常
throw new AssertionError("Unknown operation: " + this);
}
}


這段程式碼確實實現了我們的需求,但是有兩個弊端。

首先是我們不得不在最後丟擲異常或者在switch里加上default,不然無法編譯通過,但是很明顯,程式的分支是不會進入異常或者default的。

其次,這段程式碼非常脆弱,如果我們添加了新的操作型別,卻忘了在switch裡新增相應的處理邏輯,執行新的運算操作時,就會出現問題。

還好,Java列舉提供了一種功能,叫做 特定於常量的方法實現。

我們只需要在列舉型別中宣告一個抽象方法,然後在各個列舉常量中去覆蓋這個方法,實現如下:


public enum Operation {
PLUS {
double apply(double x, double y) {
return x + y;
}
},
MINUS {
double apply(double x, double y) {
return x - y;
}
},
TIMES {
double apply(double x, double y) {
return x * y;
}
},
DIVIDE {
double apply(double x, double y) {
return x / y;
}
};

abstract double apply(double x, double y);
}


這樣,也就再也不會出現新增新操作符後忘記新增對應的處理邏輯的情況了,因為編譯器就會提示我們必須覆蓋apply方法。

不過,這種 特定於常量的方法實現 有一個缺點,那就是你很難在列舉常量之間共享程式碼。

我們以星期X的列舉為例,週一到週五是工作日,執行一種邏輯,週六週日,休息日,執行另一種邏輯。

如果還是使用 特定於常量的方法實現,寫出來的程式碼可能就是這樣的:


public enum DayUseAbstractMethod {
MONDAY {
@Override
void apply() {
dealWithWeekDays();//虛擬碼
}
},
TUESDAY {
@Override
void apply() {
dealWithWeekDays();//虛擬碼
}
},
WEDNESDAY {
@Override
void apply() {
dealWithWeekDays();//虛擬碼
}
},
THURSDAY {
@Override
void apply() {
dealWithWeekDays();//虛擬碼
}
},
FRIDAY {
@Override
void apply() {
dealWithWeekDays();//虛擬碼
}
},
SATURDAY {
@Override
void apply() {
dealWithWeekEnds();//虛擬碼
}
},
SUNDAY {
@Override
void apply() {
dealWithWeekEnds();//虛擬碼
}
};

abstract void apply();
}
很明顯,我們這段程式碼裡面有相當多的重複程式碼。
那麼要怎麼優化呢,我們不妨這樣想,星期一星期二等等是一種列舉,那麼工作日和休息日,難道不也是一種列舉嗎,我們能不能給Day的建構函式傳入一個工作日休息日的DayType列舉呢?這也就是書中給出的一種叫策略列舉 的方法,程式碼如下:


public enum Day {
MONDAY(DayType.WEEKDAY), TUESDAY(DayType.WEEKDAY), WEDNESDAY(
DayType.WEEKDAY), THURSDAY(DayType.WEEKDAY), FRIDAY(DayType.WEEKDAY), SATURDAY(
DayType.WEEKDAY), SUNDAY(DayType.WEEKDAY);
private final DayType dayType;

Day(DayType daytype) {
this.dayType = daytype;
}

void apply() {
dayType.apply();
}

private enum DayType {
WEEKDAY {
@Override
void apply() {
System.out.println("hi, weekday");
}
},
WEEKEND {
@Override
void apply() {
System.out.println("hi, weekend");
}
};
abstract void apply();
}
}
通過策略列舉的方式,我們把Day的處理邏輯委託給了DayType,箇中奧妙,讀者可以細細體會。

 

列舉集合   EnumSet的使用
EnumSet提供了非常方便的方法來建立列舉集合,下面這段程式碼,感受一下


public class Text {
public enum Style {
BOLD, ITALIC, UNDERLINE, STRIKETHROUGH
}

// Any Set could be passed in, but EnumSet is clearly best
public void applyStyles(Set<Style> styles) {
// Body goes here
for(Style style : styles){
System.out.println(style);
}
}

// Sample use
public static void main(String[] args) {
Text text = new Text();
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
}
}
這個例子裡,我們使用了EnumSet.of方法,輕鬆建立了列舉集合。

 

列舉Map   EnumMap的使用
假設對於香草(Herb),有一個列舉屬性Type(一年生、多年生、兩年生)

Herb:


public class Herb {
public enum Type { ANNUAL, PERENNIAL, BIENNIAL }

private final String name;
private final Type type;

Herb(String name, Type type) {
this.name = name;
this.type = type;
}

@Override public String toString() {
return name;
}
}
現在,假設我們有一個Herb陣列,我們需要對這個Herb陣列按照Type進行分類存放。
所以接下來,我們需要建立一個Map,value肯定是Herb的集合了,那麼用什麼作為key呢?

有的人會使用列舉型別的ordinal()方法,這個函式返回int型別,表示列舉遍歷在列舉類裡的位置,這樣做,缺點很明顯,由於你的key的型別是int,不能保證傳入的int一定能和列舉類裡的變數對應上。

所以,在key的選擇上,毫無疑問,只能使用列舉型別,也即Herb.Type。

最後還有一個問題,要使用什麼Map? Java為列舉型別專門提供了一種Map,叫EnumMap,相比較與其他Map,這種Map在處理列舉型別上更快,有興趣的同學可以研究一下這個map的內部實現。

下面讓我們看看怎麼使用EnumMap:

public static void main(String[] args) {
    Herb[] garden = { new Herb("Basil", Type.ANNUAL),
            new Herb("Carroway", Type.BIENNIAL),
            new Herb("Dill", Type.ANNUAL),
            new Herb("Lavendar", Type.PERENNIAL),
            new Herb("Parsley", Type.BIENNIAL),
            new Herb("Rosemary", Type.PERENNIAL) };

    // Using an EnumMap to associate data with an enum - Page 162
    Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<Herb.Type, Set<Herb>>(
            Herb.Type.class);
    for (Herb.Type t : Herb.Type.values())
        herbsByType.put(t, new HashSet<Herb>());
    for (Herb h : garden)
        herbsByType.get(h.type).add(h);
    System.out.println(herbsByType);

}

總結
和int列舉相比,Enum列舉的在型別安全和使用便利上的優勢是不言而喻的。

Enum為列舉提供了豐富的功能,如文章中提到的特定於常量的方法實現和策略列舉。

EnumSet和EnumMap是兩個為列舉而設計的集合,在實際開發中,用到列舉集合時,請優先考慮這兩個。

 

參考文獻

《Effective Java》第二版