本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結
前面系列,我們介紹了Java中表示和運算元據的基本資料型別、類和介面,本節探討Java中的列舉型別。
所謂列舉,是一種特殊的資料,它的取值是有限的,可以列舉出來的,比如說一年就是有四季、一週有七天,雖然使用類也可以處理這種資料,但列舉型別更為簡潔、安全和方便。
下面我們就來介紹列舉的使用,同時介紹其實現原理。
基礎
基本用法
定義和使用基本的列舉是比較簡單的,我們來看個例子,為表示衣服的尺寸,我們定義一個列舉型別Size,包括三個尺寸,小/中/大,程式碼如下:
public enum Size {
SMALL, MEDIUM, LARGE
}
複製程式碼
列舉使用enum這個關鍵字來定義,Size包括三個值,分別表示小、中、大,值一般是大寫的字母,多個值之間以逗號分隔。列舉型別可以定義為一個單獨的檔案,也可以定義在其他類內部。
可以這樣使用Size:
Size size = Size.MEDIUM
複製程式碼
Size size宣告瞭一個變數size,它的型別是Size,size=Size.MEDIUM將列舉值MEDIUM賦值給size變數。
列舉變數的toString方法返回其字面值,所有列舉型別也都有一個name()方法,返回值與toString()一樣,例如:
Size size = Size.SMALL;
System.out.println(size.toString());
System.out.println(size.name());
複製程式碼
輸出都是SMALL。
列舉變數可以使用equals和==進行比較,結果是一樣的,例如:
Size size = Size.SMALL;
System.out.println(size==Size.SMALL);
System.out.println(size.equals(Size.SMALL));
System.out.println(size==Size.MEDIUM);
複製程式碼
上面程式碼的輸出結果為三行,分別是true, true, false。
列舉值是有順序的,可以比較大小。列舉型別都有一個方法int ordinal(),表示列舉值在宣告時的順序,從0開始,例如,如下程式碼輸出為1:
Size size = Size.MEDIUM;
System.out.println(size.ordinal());
複製程式碼
另外,列舉型別都實現了Java API中的Comparable介面,都可以通過方法compareTo與其他列舉值進行比較,比較其實就是比較ordinal的大小,例如,如下程式碼輸出為-1,表示SMALL小於MEDIUM:
Size size = Size.SMALL;
System.out.println(size.compareTo(Size.MEDIUM));
複製程式碼
列舉變數可以用於和其他型別變數一樣的地方,如方法引數、類變數、例項變數等,列舉還可以用於switch語句,程式碼如下所示:
static void onChosen(Size size){
switch(size){
case SMALL:
System.out.println("chosen small"); break;
case MEDIUM:
System.out.println("chosen medium"); break;
case LARGE:
System.out.println("chosen large"); break;
}
}
複製程式碼
在switch語句內部,列舉值不能帶列舉型別字首,例如,直接使用SMALL,不能使用Size.SMALL。
列舉型別都有一個靜態的valueOf(String)方法,可以返回字串對應的列舉值,例如,以下程式碼輸出為true:
System.out.println(Size.SMALL==Size.valueOf("SMALL"));
複製程式碼
列舉型別也都有一個靜態的values方法,返回一個包括所有列舉值的陣列,順序與宣告時的順序一致,例如:
for(Size size : Size.values()){
System.out.println(size);
}
複製程式碼
螢幕輸出為三行,分別是SMALL, MEDIUM, LARGE。
列舉的好處
Java是從JDK 5才開始支援列舉的,在此之前,一般是在類中定義靜態整形變數來實現類似功能,程式碼如下所示:
class Size {
public static final int SMALL = 0;
public static final int MEDIUM = 1;
public static final int LARGE = 2;
}
複製程式碼
列舉的好處是比較明顯的:
- 定義列舉的語法更為簡潔。
- 列舉更為安全,一個列舉型別的變數,它的值要麼為null,要麼為列舉值之一,不可能為其他值,但使用整形變數,它的值就沒有辦法強制,值可能就是無效的。
- 列舉型別自帶很多便利方法(如values, valueOf, toString等),易於使用。
基本實現原理
列舉型別實際上會被Java編譯器轉換為一個對應的類,這個類繼承了Java API中的java.lang.Enum類。
Enum類有兩個例項變數name和ordinal,在構造方法中需要傳遞,name(), toString(), ordinal(), compareTo(), equals()方法都是由Enum類根據其例項變數name和ordinal實現的。
values和valueOf方法是編譯器給每個列舉型別自動新增的,上面的列舉型別Size轉換後的普通類的程式碼大概如下所示:
public final class Size extends Enum<Size> {
public static final Size SMALL = new Size("SMALL",0);
public static final Size MEDIUM = new Size("MEDIUM",1);
public static final Size LARGE = new Size("LARGE",2);
private static Size[] VALUES =
new Size[]{SMALL,MEDIUM,LARGE};
private Size(String name, int ordinal){
super(name, ordinal);
}
public static Size[] values(){
Size[] values = new Size[VALUES.length];
System.arraycopy(VALUES, 0,
values, 0, VALUES.length);
return values;
}
public static Size valueOf(String name){
return Enum.valueOf(Size.class, name);
}
}
複製程式碼
解釋幾點:
- Size是final的,不能被繼承,Enum表示父類,是泛型寫法,我們後續文章介紹,此處可以忽略。
- Size有一個私有的構造方法,接受name和ordinal,傳遞給父類,私有表示不能在外部建立新的例項。
- 三個列舉值實際上是三個靜態變數,也是final的,不能被修改。
- values方法是編譯器新增的,內部有一個values陣列保持所有列舉值。
- valueOf方法呼叫的是父類的方法,額外傳遞了引數Size.class,表示類的型別資訊,型別資訊我們後續文章介紹,父類實際上是回過頭來呼叫values方法,根據name對比得到對應的列舉值的。
一般列舉變數會被轉換為對應的類變數,在switch語句中,列舉值會被轉換為其對應的ordinal值。
可以看出,列舉型別本質上也是類,但由於編譯器自動做了很多事情,它的使用也就更為簡潔、安全和方便。
典型場景
用法
以上列舉用法是最簡單的,實際中列舉經常會有關聯的例項變數和方法,比如說,上面的Size例子,每個列舉值可能有關聯的縮寫和中文名稱,可能需要靜態方法根據縮寫返回對應的列舉值,修改後的Size程式碼如下所示:
public enum Size {
SMALL("S","小號"),
MEDIUM("M","中號"),
LARGE("L","大號");
private String abbr;
private String title;
private Size(String abbr, String title){
this.abbr = abbr;
this.title = title;
}
public String getAbbr() {
return abbr;
}
public String getTitle() {
return title;
}
public static Size fromAbbr(String abbr){
for(Size size : Size.values()){
if(size.getAbbr().equals(abbr)){
return size;
}
}
return null;
}
}
複製程式碼
以上程式碼定義了兩個例項變數abbr和title,以及對應的get方法,分別表示縮寫和中文名稱,定義了一個私有構造方法,接受縮寫和中文名稱,每個列舉值在定義的時候都傳遞了對應的值,同時定義了一個靜態方法fromAbbr根據縮寫返回對應的列舉值。
需要說明的是,列舉值的定義需要放在最上面,列舉值寫完之後,要以分號(;)結尾,然後才能寫其他程式碼。
這個列舉定義的使用與其他類類似,比如說:
Size s = Size.MEDIUM;
System.out.println(s.getAbbr());
s = Size.fromAbbr("L");
System.out.println(s.getTitle());
複製程式碼
以上程式碼分別輸出: M, 大號
實現原理
加了例項變數和方法後,列舉轉換後的類與上面的類似,只是增加了對應的變數和方法,修改了構造方法,程式碼不同之處大概如下所示:
public final class Size extends Enum<Size> {
public static final Size SMALL =
new Size("SMALL",0, "S", "小號");
public static final Size MEDIUM =
new Size("MEDIUM",1,"M","中號");
public static final Size LARGE =
new Size("LARGE",2,"L","大號");
private String abbr;
private String title;
private Size(String name, int ordinal,
String abbr, String title){
super(name, ordinal);
this.abbr = abbr;
this.title = title;
}
//... 其他程式碼
}
複製程式碼
說明
每個列舉值經常有一個關聯的標示(id),通常用int整數表示,使用整數可以節約儲存空間,減少網路傳輸。一個自然的想法是使用列舉中自帶的ordinal值,但ordinal並不是一個好的選擇。
為什麼呢?因為ordinal的值會隨著列舉值在定義中的位置變化而變化,但一般來說,我們希望id值和列舉值的關係保持不變,尤其是表示列舉值的id已經儲存在了很多地方的時候。
比如說,上面的Size例子,Size.SMALL的ordinal的值為0,我們希望0表示的就是Size.SMALL的,但如果我們增加一個表示超小的值XSMALL呢?
public enum Size {
XSMALL, SMALL, MEDIUM, LARGE
}
複製程式碼
這時,0就表示XSMALL了。
所以,一般是增加一個例項變數表示id,使用例項變數的另一個好處是,id可以自己定義。比如說,Size例子可以寫為:
public enum Size {
XSMALL(10), SMALL(20), MEDIUM(30), LARGE(40);
private int id;
private Size(int id){
this.id = id;
}
public int getId() {
return id;
}
}
複製程式碼
高階用法
列舉還有一些高階用法,比如說,每個列舉值可以有關聯的類定義體,列舉型別可以宣告抽象方法,每個列舉值中可以實現該方法,也可以重寫列舉型別的其他方法。
比如說,我們看改後的Size程式碼(這個程式碼實際意義不大,主要展示語法):
public enum Size {
SMALL {
@Override
public void onChosen() {
System.out.println("chosen small");
}
},MEDIUM {
@Override
public void onChosen() {
System.out.println("chosen medium");
}
},LARGE {
@Override
public void onChosen() {
System.out.println("chosen large");
}
};
public abstract void onChosen();
}
複製程式碼
Size列舉型別定義了onChosen抽象方法,表示選擇了該尺寸後執行的程式碼,每個列舉值後面都有一個類定義體{},都重寫了onChosen方法。
這種寫法有什麼好處呢?如果每個或部分列舉值有一些特定的行為,使用這種寫法比較簡潔。對於這個例子,上面我們介紹了其對應的switch語句,在switch語句中根據size的值執行不同的程式碼。
switch的缺陷是,定義swich的程式碼和定義列舉型別的程式碼可能不在一起,如果新增了列舉值,應該需要同樣修改switch程式碼,但可能會忘記,而如果使用抽象方法,則不可能忘記,在定義列舉值的同時,編譯器會強迫同時定義相關行為程式碼。所以,如果行為程式碼和列舉值是密切相關的,使用以上寫法可以更為簡潔、安全、容易維護。
這種寫法內部是怎麼實現的呢?每個列舉值都會生成一個類,這個類繼承了列舉型別對應的類,然後再加上值特定的類定義體程式碼,列舉值會變成這個子類的物件,具體程式碼我們就不贅述了。
列舉還有一些其他高階用法,比如說,列舉可以實現介面,也可以在介面中定義列舉,使用相對較少,本文就不介紹了。
小結
本節介紹了列舉型別,介紹了基礎用法、典型場景及高階用法,不僅介紹瞭如何使用,還介紹了實現原理,對於列舉型別的資料,雖然直接使用類也可以處理,但列舉型別更為簡潔、安全和方便。
我們之前提到過異常,但並未深入討論,讓我們下節來探討。
未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。