前面系列,我們介紹了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; }
枚舉的好處是比較明顯的:
基本實現原理
枚舉類型實際上會被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); } }
解釋幾點:
一般枚舉變量會被轉換為對應的類變量,在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編程及計算機技術的本質。用心寫作,原創文章,保留所有版權。
-----------
更多相關原創文章
計算機程序的思維邏輯 (13) - 類
計算機程序的思維邏輯 (14) - 類的組合
計算機程序的思維邏輯 (15) - 初識繼承和多態
計算機程序的思維邏輯 (16) - 繼承的細節
計算機程序的思維邏輯 (17) - 繼承實現的基本原理
計算機程序的思維邏輯 (18) - 為什麼說繼承是把雙刃劍
計算機程序的思維邏輯 (19) - 接口的本質
計算機程序的思維邏輯 (20) - 為什麼要有抽象類?
計算機程序的思維邏輯 (21) - 內部類的本質