枚舉和注解都是在Java1.5中引入的,雖然它們是後起之秀,但其功效不可小觑,枚舉改變了常量的聲明方式,注解耦合了數據和代碼。
常量聲明是每一個項目都不可或缺的,在Java1.5之前,我們只有兩種方式的聲明:類常量和接口常量,若在項目中使用的是Java1.5之前的版本,基本上都是如此定義的。不過,在1.5版本以後有了改進,即新增了一種常量聲明方式:枚舉聲明常量,看如下代碼:
enum Season { Spring, Summer, Autumn, Winter; }
這是一個簡單的枚舉常量命名,清晰又簡單。順便提一句,JLS(Java Language Specification,Java語言規范)提倡枚舉項全部大寫,字母之間用下劃線分割,這也是從常量的角度考慮的(當然,使用類似類名的命名方式也是比較友好的)。
那麼枚舉常量與我們經常使用的類常量和靜態常量相比有什麼優勢?問得好,枚舉的優點主要表現在四個方面:
1.枚舉常量簡單:簡不簡單,我們來對比一下兩者的定義和使用情況就知道了。先把Season枚舉翻寫成接口常量,代碼如下:
1 interface Season { 2 int SPRING = 0; 3 int SUMMER = 1; 4 int AUTUMN = 2; 5 int WINTER = 3; 6 }
此處定義了春夏秋冬四個季節,類型都是int,這與Season枚舉的排序值是相同的。首先對比一下兩者的定義,枚舉常量只需定義每個枚舉項,不需要定義枚舉值,而接口常量(或類常量)則必須定義值,否則編譯不通過,即使我們不需要關注其值是多少也必須定義;其次,雖然兩者被引用的方式相同(都是 “類名 . 屬性”,如Season.SPRING),但是枚舉表示的是一個枚舉項,字面含義是春天,而接口常量確是一個int類型,雖然其字面含義也是春天,但在運算中我們勢必要關注其int值。
2.枚舉常量屬於穩態型
例如我們要描述一下春夏秋冬是什麼樣子,使用接口常量應該是這樣寫。
1 public void describe(int s) { 2 // s變量不能超越邊界,校驗條件 3 if (s >= 0 && s < 4) { 4 switch (s) { 5 case Season.SPRING: 6 System.out.println("this is spring"); 7 break; 8 case Season.SUMMER: 9 System.out.println("this is summer"); 10 break; 11 ...... 12 } 13 } 14 }
很簡單,先使用switch語句判斷哪一個是常量,然後輸出。但問題是我們得對輸入值進行檢查,確定是否越界,如果常量非常龐大,校驗輸入就成了一件非常麻煩的事情,但這是一個不可逃避的過程,特別是如果我們的校驗條件不嚴格,雖然編譯能照樣通過,但是運行期就會產生無法預知的後果。
我們再來看看枚舉常量是否能夠避免校驗的問題,代碼如下:
1 public void describe(Season s){ 2 switch(s){ 3 case Spring: 4 System.out.println("this is "+Season.Spring); 5 break; 6 case Summer: 7 System.out.println("this is summer"+Season.Summer); 8 break; 9 ...... 10 } 11 } 12
不用校驗,已經限定了是Season枚舉,所以只能是Season類的四個實例,即春夏秋冬4個枚舉項,想輸入一個int類型或其它類型?門都沒有!這是我們最看重枚舉的地方:在編譯期間限定類型,不允許發生越界的情況。
3.枚舉具有內置方法
有一個簡單的問題:如果要列出所有的季節常量,如何實現呢?接口常量或類常量可以通過反射來實現,這沒錯,只是雖然能實現,但會非常繁瑣,大家可以自己寫一個反射類實現此功能(當然,一個一個地動手打印出輸出常量,也可以算是列出)。對於此類問題可以非常簡單的解決,代碼如下:
1 public void query() { 2 for (Season s : Season.values()) { 3 System.out.println(s); 4 } 5 }
通過values方法獲得所有的枚舉項,然後打印出來即可。如此簡單,得益於枚舉內置的方法,每個枚舉都是java.lang.Enum的子類,該基類提供了諸如獲得排序值的ordinal方法、compareTo比較方法等,大大簡化了常量的訪問。
4.枚舉可以自定義的方法
這一點似乎並不是枚舉的優點,類常量也可以有自己的方法呀,但關鍵是枚舉常量不僅可以定義靜態方法,還可以定義非靜態方法,而且還能夠從根本上杜絕常量類被實例化。比如我們要在常量定義中獲得最舒服季節的方法,使用常量枚舉的代碼如下:
1 enum Season { 2 Spring, Summer, Autumn, Winter; 3 public static Season getComfortableSeason(){ 4 return Spring; 5 } 6 }
我們知道,每個枚舉項都是該枚舉的一個實例,對於我們的例子來說,也就表示Spring其實是Season的一個實例,Summer也是其中一個實例,那我們在枚舉中定義的靜態方法既可以在類(也就是枚舉Season)中引用,也可以在實例(也就是枚舉項Spring、Summer、Autumn、Winter)中引用,看如下代碼:
public static void main(String[] args) { System.out.println("The most comfortable season is "+Season.getComfortableSeason()); }
那如果使用類常量要如何實現呢?代碼如下:
1 class Season { 2 public final static int SPRING = 0; 3 public final static int SUMMER = 1; 4 public final static int AUTUMN = 2; 5 public final static int WINTER = 3; 6 public static int getComfortableSeason(){ 7 return SPRING; 8 } 9 }
想想看,我們怎麼才能打印出"The most comfortable season is Spring" 這句話呢?除了使用switch和if判斷之外沒有其它辦法了。
雖然枚舉在很多方面比接口常量和類常量好用,但是有一點它是比不上接口常量和類常量的,那就是繼承,枚舉類型是不能繼承的,也就是說一個枚舉常量定義完畢後,除非修改重構,否則無法做擴展,而接口常量和類常量則可以通過繼承進行擴展。但是,一般常量在項目構建時就定義完畢了,很少會出現必須通過擴展才能實現業務邏輯的場景。
注意: 在項目中推薦使用枚舉常量代替接口常量或類常量。
一般來說,我們經常使用的枚舉項只有一個屬性,即排序號,其默認值是從0、1、2......,這一點我們很熟悉,但是除了排序號之外,枚舉還有一個(或多個)屬性:枚舉描述,他的含義是通過枚舉的構造函數,聲明每個枚舉項(也就是枚舉的實例)必須具有的屬性和行為,這是對枚舉項的描述或補充,目的是使枚舉項描述的意義更加清晰准確。例如有這樣一段代碼:
1 public enum Season { 2 Spring("春"), Summer("夏"), Autumn("秋"), Winter("冬"); 3 private String desc; 4 5 Season(String _desc) { 6 desc = _desc; 7 } 8 //獲得枚舉描述 9 public String getDesc() { 10 return desc; 11 } 12 }
其枚舉選項是英文的,描述是中文的,如此設計使其表述的意義更加精確,方便了多個作者共同引用該常量。若不考慮描述的使用(即訪問getDesc方法),它與如下接口定義的描述很相似:
interface Season{ //春 int SPRING =0; //夏 int SUMMER =1; //...... }
比較兩段代碼,很容易看出使用枚舉項描述是一個很好的解決辦法,非常簡單、清晰。因為是一個描述(Description),那我們在開發時就可以賦予更多的含義,比如可以通過枚舉構造函數聲明業務值,定義可選項,添加屬性等,看如下代碼:
1 enum Role { 2 Admin("管理員", new LifeTime(), new Scope()), User("普通用戶", new LifeTime(), new Scope()); 3 private String name; 4 private LifeTime lifeTime; 5 private Scope scope; 6 /* setter和getter方法略 */ 7 8 Role(String _name, LifeTime _lifeTime, Scope _scope) { 9 name = _name; 10 lifeTime = _lifeTime; 11 scope = _scope; 12 } 13 14 } 15 16 class LifeTime { 17 } 18 class Scope { 19 }
這是一個角色定義類,描述了兩個角色:管理員和普通用戶,同時它還通過構造函數對這兩個角色進行了描述:
大家可以看出,這樣一個描述可以使開發者對Admin和User兩個常量有一個立體多維度的認知,有名稱,有周期,還有范圍,而且還可以在程序中方便的獲得此類屬性。所以,推薦大家在枚舉定義中為每個枚舉項定義描述,特別是在大規模的項目開發中,大量的常量定義使用枚舉項描述比在接口常量或類常量中增加注釋的方式友好的多,簡潔的多。
使用枚舉定義常量時。會伴有大量switch語句判斷,目的是為了每個枚舉項解釋其行為,例如這樣一個方法:
1 public static void doSports(Season season) { 2 switch (season) { 3 case Spring: 4 System.out.println("春天放風筝"); 5 break; 6 case Summer: 7 System.out.println("夏天游泳"); 8 break; 9 case Autumn: 10 System.out.println("秋天是收獲的季節"); 11 break; 12 case Winter: 13 System.out.println("冬天滑冰"); 14 break; 15 default: 16 System.out.println("輸出錯誤"); 17 break; 18 } 19 }
上面的代碼傳入了一個Season類型的枚舉,然後使用switch進行匹配,目的是輸出每個季節的活動,現在的問題是這段代碼又沒有問題:
我們先來看看它是如何被調用的,因為要傳遞進來的是Season類型,也就是一個實例對象,那當然允許為空了,我們就傳遞一個null值進去看看代碼又沒有問題,如下:
public static void main(String[] args) { doSports(null); }
似乎會打印出“輸出錯誤”,因為switch中沒有匹配到指定值,所以會打印出defaut的代碼塊,是這樣的嗎?不是,運行後的結果如下:
Exception in thread "main" java.lang.NullPointerException at com.book.study85.Client85.doSports(Client85.java:8) at com.book.study85.Client85.main(Client85.java:28)
竟然是空指針異常,也就是switch的那一行,怎麼會有空指針呢?這就與枚舉和switch的特性有關了,此問題也是在開發中經常發生的。我們知道,目前Java中的switch語句只能判斷byte、short、char、int類型(JDk7允許使用String類型),這是Java編譯器的限制。問題是為什麼枚舉類型也可以跟在switch後面呢?
因為編譯時,編譯器判斷出switch語句後跟的參數是枚舉類型,然後就會根據枚舉的排序值繼續匹配,也就是或上面的代碼與以下代碼相同:
1 public static void doSports(Season season) { 2 switch (season.ordinal()) { 3 case season.Spring.ordinal(): 4 System.out.println("春天放風筝"); 5 break; 6 case season.Summer.ordinal(): 7 System.out.println("夏天游泳"); 8 break; 9 //...... 10 } 11 }
看明白了吧,switch語句是先計算season變量的排序值,然後與枚舉常量的每個排序值進行對比,在我們的例子中season是null,無法執行ordinal()方法,於是就報空指針異常了。問題清楚了,解決很簡單,在doSports方法中判斷輸入參數是否為null即可。
switch後跟枚舉類型,case後列出所有的枚舉項,這是一個使用枚舉的主流寫法,那留著default語句似乎沒有任何作用,程序已經列舉了所有的可能選項,肯定不會執行到defaut語句,看上去純屬多余嘛!錯了,這個default還是很有作用的。以我們定義的日志級別來說明,這是一個典型的枚舉常量,如下所示:
enum LogLevel{ DEBUG,INFO,WARN,ERROR }
一般在使用的時候,會通過switch語句來決定用戶設置的日志級別,然後輸出不同級別的日志代碼,代碼如下:
1 switch(LogLevel) 2 3 { 4 case:DEBUG: 5 //..... 6 case:INFO: 7 //...... 8 case:WARN: 9 //...... 10 case:ERROR: 11 //...... 12 }
由於把所有的枚舉項都列舉完了,不可能有其它值,所以就不需要default代碼快了,這是普遍認識,但問題是我們的switch代碼與枚舉之間沒有強制約束關系,也就是說兩者只是在語義上建立了聯系,並沒有一個強制約束,比如LogLevel的枚舉項發生變化了,增加了一個枚舉項FATAL,如果此時我們對switch語句不做任何修改,編譯雖不會出問題,但是運行期會發生非預期的錯誤:FATAL類型的日志沒有輸出。
為了避免出現這類錯誤,建議在default後直接拋出一個AssertionError錯誤,其含義就是“不要跑到這裡來,一跑到這裡就會出問題”,這樣可以保證在增加一個枚舉項的情況下,若其它代碼未修改,運行期馬上就會出錯,這樣一來就很容易找到錯誤,方便立即排除。
當然也有其它方法解決此問題,比如修改IDE工具,以Eclipse為例,可以把Java-->Compiler--->Errors/Warnings中的“Enum type constant not covered on 'switch' ”設置為Error級別,如果不判斷所有的枚舉項就不能編譯通過。
我們知道每個枚舉項都是java.lang.Enum的子類,都可以訪問Enum類提供的方法,比如hashCode、name、valueOf等,其中valueOf方法會把一個String類型的名稱轉換為枚舉項,也就是在枚舉項中查找出字面值與參數相等的枚舉項。雖然這個方法簡單,但是JDK卻做了一個對於開發人員來說並不簡單的處理,我們來看代碼:
1 public static void main(String[] args) { 2 // 注意summer是小寫 3 List<String> params = Arrays.asList("Spring", "summer"); 4 for (String name : params) { 5 // 查找字面值與name相同的枚舉項,其中Season是前面例子中枚舉Season 6 Season s = Season.valueOf(name); 7 if (null != s) { 8 // 有枚舉項時 9 System.out.println(s); 10 } else { 11 // 沒有該枚舉項 12 System.out.println("無相關枚舉項"); 13 } 14 } 15 }
這段程序看起來沒什麼錯吧,其中考慮到從String轉換為枚舉類型可能存在著轉換不成功的情況,比如沒有匹配找到指定值,此時ValueOf的返回值應該為空,所以後面又跟著if...else判斷輸出。我們看看運行結果
Spring Exception in thread "main" java.lang.IllegalArgumentException: No enum constant com.book.study01.Season.summer at java.lang.Enum.valueOf(Unknown Source) at com.book.study01.Season.valueOf(Season.java:1) at com.book.study85.Client85.main(Client85.java:14)
報無效的參數異常,也就說我們的summer(注意s是小寫),無法轉換為Season枚舉,無法轉換就 不轉換嘛,那也別拋出IllegalArgumentException異常啊,一但拋出這個異常,後續的代碼就不會執行了,這與我們的習慣不符合呀,例如我們從List中查找一個元素,即使不存在也不會報錯,頂多indexOf方法返回-1。那麼我們來深入分析一下該問題,valueOf方法的源代碼如下:
1 public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { 2 //通過反射,從常量列表中查找 3 T result = enumType.enumConstantDirectory().get(name); 4 if (result != null) 5 return result; 6 if (name == null) 7 throw new NullPointerException("Name is null"); 8 //最後拋出無效參數異常 9 throw new IllegalArgumentException("No enum constant " + enumType.getCanonicalName() + "." + name); 10 }
valueOf方法先通過反射從枚舉類的常量聲明中查找,若找到就直接返回,若找不到則拋出無效參數異常。valueOf的本意是保護編碼中的枚舉安全性,使其不產生空枚舉對象,簡化枚舉操作,但是卻引入了一個我們無法避免的IllegalArgumentException異常。
大家是否覺得此處的valueOf方法的源碼不對,這裡要輸入兩個參數,而我們的Season.valueOf只傳遞一個String類型的參數,真的是這樣嗎?是的,因為valueOf(String name)方法是不可見的,是JVM內置的方法,我們只有通過閱讀公開的valueOf方法來了解其運行原理了。
問題清楚了,有兩個方法可以解決此問題:
(1)、使用try......catch捕捉異常
這裡是最直接也是最簡單的方式,產生IllegalArgumentException即可確認為沒有同名的枚舉的枚舉項,代碼如下:
try{ Season s = Season.valueOf(name); //有該枚舉項時 System.out.println(s); }catch(Exception e){ e.printStackTrace(); System.out.println("無相關枚舉項"); }
(2)、擴展枚舉類:由於Enum類定義的方法基本上都是final類型的,所以不希望被覆寫,我們可以學習String和List,通過增加一個contains方法來判斷是否包含指定的枚舉項,然後再繼續轉換,代碼如下。
1 enum Season { 2 Spring, Summer, Autumn, Winter; 3 // 是否包含指定的枚舉項 4 public static boolean contains(String name) { 5 // 所有的枚舉值 6 Season[] season = values(); 7 for (Season s : season) { 8 if (s.name().equals(name)) { 9 return true; 10 } 11 } 12 return false; 13 } 14 }
Season枚舉具備了靜態方法contains後,就可以在valueOf前判斷一下是否包含指定的枚舉名稱了,若包含則可以通過valueOf轉換為枚舉,若不包含則不轉換。