Java高階系列——列舉(Enums)
一、介紹
本系列文章的這一部分我們將會介紹Java 5版本之後引入的除泛型之外的另外一個強大特性:列舉。可以將列舉看成一種特殊的類,並且可以將註解看成一種特殊的介面。
列舉的思想很簡單,也很方便:它代表了一組固定的常量值。實際上,列舉經常用來設計一些狀態常量。比如,星期幾就是列舉的一個最好例子,因為他們被限制在週一、週二、週三、週四、週五、 週六和週日。
二、列舉作為特殊的類
在列舉被引入Java語言之前,在Java中模擬一組固定值的常規方法是通過宣告一組常量。例如:
public class DaysOfTheWeekConstants {
public static final int MONDAY = 0;
public static final int TUESDAY = 1;
public static final int WEDNESDAY = 2;
public static final int THURSDAY = 3;
public static final int FRIDAY = 4;
public static final int SATURDAY = 5;
public static final int SUNDAY = 6;
}
雖然這種方法有效,但遠非理想的解決方案。主要是因為常量本身只是int型別的值,而程式碼中需要這些常量(而不是任意的int值)的每一個地方都應該被一直明確地記錄和斷言。從語義上來講,比如下面的這個方法演示所表現出來的就不符合型別安全的概念:
public boolean isWeekend( int day ) {
return( day == SATURDAY || day == SUNDAY );
}
從邏輯角度去看,day引數應該是在DaysOfTheWeekConstants類中宣告的值之一。然而,如果沒有編寫額外的說明文件(給後來的一些人閱讀),就不可能猜測到這一點。對於Java編譯器來說類似於isWeekend(100)的這種呼叫看起來完全是正確的並且不會引起任何顧慮。
此時列舉就能解決這些問題。列舉允許用型別化的值替換常量並在任何地方使用這些型別。讓我們使用列舉重寫上面的方案。
public enum DaysOfTheWeek {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
}
這裡將關鍵字class改成了enum並且這些可能的值在列舉定義時被列舉出來。有區別的一部分就是被宣告在列舉類(在我們的例子中是DaysOfTheWeek )中的每一個單獨的值都是一個例項。因此,每當列舉被使用,Java編譯器都能夠進行型別檢查。比如:
public boolean isWeekend(DaysOfTheWeek day) {
return(day == SATURDAY || day == SUNDAY);
}
請注意列舉中的大寫命名法的使用是一個約定,但是如果你不這樣做也沒有誰能夠阻止你,但是最好還是遵守約定,這樣更有利於程式碼的維護。
三、列舉與例項欄位(Enums and instance fields)
列舉是一個特殊的類,因此它是可拓展的。這意味著他們可以有例項欄位、構造器和方法(預設無參構造器不能夠被宣告並且所有的構造器必須被private修飾)。讓我們使用列舉的例項和構造器新增一個isWeekend屬性。
public enum DaysOfTheWeekFields {
MONDAY(false),
TUESDAY(false),
WEDNESDAY(false),
THURSDAY(false),
FRIDAY(false),
SATURDAY(true),
SUNDAY(true);
private final boolean isWeekend;
private DaysOfTheWeekFields( final boolean isWeekend ) {
this.isWeekend = isWeekend;
}
public boolean isWeekend() {
return isWeekend;
}
}
我們看到,列舉值只是簡單的呼叫了構造器而並沒有要求使用new關鍵字。isWeekend()方法可以用來確定是否列舉值代表工作日或者週末。比如:
public boolean isWeekend( DaysOfTheWeek day ) {
return day.isWeekend();
}
Java中列舉的例項欄位有很大的用處。在常規的類宣告規則中,它們經常用來將一些額外的細節與每個值相關聯。
四、列舉與介面(Enums and interfaces)
另外一個強大的特性,我們再次確認一下列舉是是一個特殊的類,所以它能夠實現介面(然而列舉不能夠繼承任何類)。比如,讓我們引入介面DayOfWeek。
interface DayOfWeek {
boolean isWeekend();
}
然後使用介面實現代替常規例項欄位的方式重寫前面的列舉例子。
public enum DaysOfTheWeekInterfaces implements DayOfWeek {
MONDAY() {
@Override
public boolean isWeekend() {
return false;
}
},
TUESDAY() {
@Override
public boolean isWeekend() {
return false;
}
},
WEDNESDAY() {
@Override
public boolean isWeekend() {
return false;
}
},
THURSDAY() {
@Override
public boolean isWeekend() {
return false;
}
},
FRIDAY() {
@Override
public boolean isWeekend() {
return false;
}
},
SATURDAY() {
@Override
public boolean isWeekend() {
return true;
}
},
SUNDAY() {
@Override
public boolean isWeekend() {
return true;
}
};
}
我們實現介面的這種方式顯得程式碼有些冗長,然而合併例項欄位和介面實現可以解決這個問題,比如:
public enum DaysOfTheWeekFieldsInterfaces implements DayOfWeek {
MONDAY( false ),
TUESDAY( false ),
WEDNESDAY( false ),
THURSDAY( false ),
FRIDAY( false ),
SATURDAY( true ),
SUNDAY( true );
private final boolean isWeekend;
private DaysOfTheWeekFieldsInterfaces(final boolean isWeekend){
this.isWeekend = isWeekend;
}
@Override
public boolean isWeekend() {
return isWeekend;
}
}
通過支援例項欄位和介面,列舉可以以更加面向物件的方式使用,從而帶來一定程度的抽象。
五、列舉與泛型
在Java中,雖然咋一看並看不出來列舉和泛型的關係,但是他們之間存在一種關係。Java中的每一個單獨的列舉自動繼承自泛型類Enum<T>,在這裡T就是列舉型別本身。Java編譯器在編譯時代表開發者做了這個轉換,拓展一下列舉宣告public enum DaysOfTheWeek 如下:
public class DaysOfTheWeek extends Enum< DaysOfTheWeek > {
// Other declarations here
}
這也就說明了為什麼列舉可以實現介面但不能繼承其他類:因為它隱式的繼承自Enum<T>並且我們在使用物件的公共方法時已經討論過,Java中不支援多繼承。
實際上每一個繼承自Enum<T>的列舉允許定義泛型類、介面和方法,通過這種方式可以讓列舉型別的例項引數化或者型別引數化。比如:
public<T extends Enum< ? >> void performAction(final T instance) {
// Perform some action here
}
在上面的方法宣告中,型別T被約定為任意列舉型別的例項並且Java編譯器將會對其做驗證。
六、列舉方法
基礎類 Enum<T>為自動繼承它的列舉例項提供了一些非常有用的方法。
方法 | 描述 |
---|---|
String name() | 返回列舉宣告宣告的列舉常量的名稱 |
int ordinal() | 返回列舉常量的次序(即列舉宣告時的位置,初始常量分配的位置是0) |
此外,Java編譯器為每個列舉型別自動生成兩個更有用的靜態方法(讓我們將這個特殊的列舉型別假設為T)。
方法 | 描述 |
---|---|
T[] values() | 返回列舉T所宣告的所有常量 |
T valueOf(String name) | 返回指定名稱的列舉常量 |
在程式碼中使用列舉還有一個好處:可以使用switch/case語法。例如:
public void performAction( DaysOfTheWeek instance ) {
switch( instance ) {
case MONDAY:
// Do something
break;
case TUESDAY:
// Do something
break;
// Other enum constants here
}
}
七、專用集合:EnumSet和EnumMap
和所有其他類一樣,列舉的例項也可以和標準Java集合庫一起使用。然而,某些集合型別針對列舉做了優化,並且在大多數情況下推薦使用這些優化過後的集合代替通用的集合。
本節我們簡單瞭解一下兩個專用的集合:EnumSet<T>和EnumMap<T, ?>。這兩個集合都非常容易使用。
我們首先來看一下EnumSet<T>集合。EnumSet<T>集合是常規的集合優化過後高效儲存列舉型別的一個集合,EnumSet<T>不能夠使用構造器進行例項化,但是它提供了很多非常有用的工廠方法。
比如,allOf工廠方法建立的EnumSet<T>例項就包含了所有列舉型別所列舉的常量:
final Set<DaysOfTheWeek> enumSetAll = EnumSet.allOf(DaysOfTheWeek.class);
noneOf工廠方法建立的是一個空的EnumSet<T>例項:
final Set<DaysOfTheWeek> enumSetNone = EnumSet.noneOf(DaysOfTheWeek.class);
使用of工廠方法,可以指定列舉型別中那些列舉常量應該包含在EnumSet<T>中:
final Set< DaysOfTheWeek > enumSetSome = EnumSet.of(
DaysOfTheWeek.SUNDAY,
DaysOfTheWeek.SATURDAY
);
EnumMap<T, ?>是最接近於一般的map的,唯一的不同就是EnumMap<T, ?>的key是列舉型別的列舉常量。比如;
final Map<DaysOfTheWeek, String> enumMap = new EnumMap<>(DaysOfTheWeek.class);
enumMap.put(DaysOfTheWeek.MONDAY, "Lundi");
enumMap.put(DaysOfTheWeek.TUESDAY, "Mardi");
注意,和大多數集合實現一樣,EnumSet<T>和EnumMap<T, ?>不是執行緒安全的所以不能在多執行緒環境下使用。
八、何時使用列舉
自Java 5釋出以來,在解決一些固定常量集合的問題上列舉成為唯一首選和推薦的一種方式。不僅是因為它們是強型別,同時它們是可拓展並被當前的很多庫和框架所支援。