Java基礎不簡單,講一講枚舉
什么是枚舉文章已收錄Github精選,歡迎Star:https://github.com/yehongzhi/learningSummary
枚舉是JDK1.5新增的一種數據類型,是一種特殊的類,常用于表示一組常量,比如一年四季,12個月份,星期一到星期天,服務返回的錯誤碼,結算支付的方式等等。枚舉是使用enum關鍵字來定義。
枚舉的使用在使用枚舉之前我們先探討一個問題,為什么要使用枚舉。
現在有個業(yè)務場景是結算支付,有支付寶和微信支付兩種方式,1表示支付寶,2表示微信支付,還需要根據編碼(1或2)獲取相應的英文名,如果不用枚舉,我們就要這樣寫。
public?class?PayTypeUtil?{
????//支付寶
????private?static?final?int?ALI_PAY?=?1;
????//微信支付
????private?static?final?int?WECHAT_PAY?=?2;
????//根據編碼獲取支付方式的名稱
????public?String?getPayName(int?code)?{
????????if?(ALI_PAY?==?code)?{
????????????return?"Ali_Pay";
????????}
????????if?(WECHAT_PAY?==?code)?{
????????????return?"Wechat_Pay";
????????}
????????return?null;
????}
}
如果這時,產品經理說要增加一個銀聯支付,就要加多if的判斷,就會造成有多少種支付方式,就有多少個if,非常難看。
如果使用枚舉,就變得很優(yōu)雅,先看代碼:
public?enum?PayTypeEnum?{
????/**?支付寶*/
????ALI_PAY(1,?"ALI_PAY"),
????/**?微信支付*/
????WECHAT_PAY(2,?"WECHAT_PAY");
????private?int?code;
????private?String?describe;
????PayTypeEnum(int?code,?String?describe)?{
????????this.code?=?code;
????????this.describe?=?describe;
????}
????//根據編碼獲取支付方式
????public?PayTypeEnum?find(int?code)?{
????????for?(PayTypeEnum?payTypeEnum?:?values())?{
????????????if?(payTypeEnum.getCode()?==?code)?{
????????????????return?payTypeEnum;
????????????}
????????}
????????return?null;
????}
????//getter、setter方法
}
當我們需要擴展,只需要定義多一個實例即可,其他代碼都不用動,比如加多一個銀聯支付。
/**?支付寶*/
ALI_PAY(1,?"ALI_PAY"),
/**?微信支付*/
WECHAT_PAY(2,?"WECHAT_PAY"),
//只需要加多一行代碼即可完成擴展
/**?銀聯支付*/
UNION_PAY(3,"UNION_PAY");
一般在實際項目中,最多的寫法就是這樣,主要是簡單明了,易于擴展。
第二種常見的用法是結合switch-case使用,比如我定義一個一年四季的枚舉。
public?enum?Season?{
????//春
????SPRING,
????//夏
????SUMMER,?
????//秋
????AUTUMN,?
????//冬
????WINTER;
}
然后結合switch使用。
public?static?void?main(String[]?args)?throws?Exception{
????doSomething(Season.SPRING);
}
private?static?void?doSomething(Season?season){
????switch?(season){
????????case?SPRING:
????????????System.out.println("不知細葉誰裁出,二月春風似剪刀");
????????????break;
????????case?SUMMER:
????????????System.out.println("接天蓮葉無窮碧,映日荷花別樣紅");
????????????break;
????????case?AUTUMN:
????????????System.out.println("停車坐愛楓林晚,霜葉紅于二月花");
????????????break;
????????case?WINTER:
????????????System.out.println("梅花香自苦寒來,寶劍鋒從磨礪出");
????????????break;
????????default:
????????????System.out.println("垂死病中驚坐起,笑問客從何處來");
????}
}
可能很多人覺得直接用int,String類型配合switch使用就夠了,為什么還要支持枚舉,這樣的設計是不是顯得很多余,其實非也。
不妨反過來想,假如用1到4代表四季,接收的參數類型就是int,在沒有提示的情況下,我們僅僅只知道數int類型是很難猜到需要傳入數字的范圍,字符串也是一樣,如果不用枚舉你是很難一眼看出需要傳入什么參數,這才是最關鍵的。
如果使用枚舉,那么問題就迎刃而解,當你調用doSomething()方法時,一看到枚舉就知道傳入的是哪幾個參數,因為已經在枚舉類里面定義好了。這對于項目交接,還有代碼的可讀性都是非常有利的。
這種限制不單止限制了調用方,也限制了傳入的參數只能是定義好的枚舉,不用擔心傳入的參數錯誤導致的程序錯誤。
所以枚舉類使用得恰當,對于項目的可維護性是有很大提升的。
枚舉本身的方法首先我們先以上面的支付類型枚舉PayTypeEnum為例子,看看有哪些自帶的方法。
valueOf()方法
這是一個靜態(tài)方法,傳入一個字符串(枚舉的名稱),獲取枚舉類。如果傳入的名稱不存在,則報錯。
public?static?void?main(String[]?args)?throws?Exception{
????System.out.println(PayTypeEnum.valueOf("ALI_PAY"));
????System.out.println(PayTypeEnum.valueOf("HUAWEI_PAY"));
}

values()方法
返回包含枚舉類中所有枚舉數據的一個數組。
public?static?void?main(String[]?args)?throws?Exception?{
????PayTypeEnum[]?payTypeEnums?=?PayTypeEnum.values();
????for?(PayTypeEnum?payTypeEnum?:?payTypeEnums)?{
????????System.out.println("code:?"?+?payTypeEnum.getCode()?+?",describe:?"?+?payTypeEnum.getDescribe());
????}
}

ordinal()方法
默認情況下,枚舉類會給定義的枚舉提供一個默認的次序,ordinal()方法就可以返回枚舉的次序。
public?static?void?main(String[]?args)?throws?Exception?{
????PayTypeEnum[]?payTypeEnums?=?PayTypeEnum.values();
????for?(PayTypeEnum?payTypeEnum?:?payTypeEnums)?{
????????System.out.println("ordinal:?"?+?payTypeEnum.ordinal()?+?",?Enum:?"?+?payTypeEnum);
????}
}
/**
ordinal:?0,?Enum:?ALI_PAY
ordinal:?1,?Enum:?WECHAT_PAY
ordinal:?2,?Enum:?UNION_PAY
*/
name()、toString()方法
返回定義枚舉用的名稱。
public?static?void?main(String[]?args)?throws?Exception?{
????for?(Season?season?:?Season.values())?{
????????System.out.println(season.name());
????}
????for?(Season?season?:?Season.values())?{
????????System.out.println(season.toString());
????}
}
輸出結果都是一樣的:
SPRING
SUMMER
AUTUMN
WINTER
為什么?因為底層代碼是一樣,返回的是name。
public?abstract?class?Enum<E?extends?Enum<E>>?implements?Comparable<E>,?Serializable?{
????
????public?final?String?name()?{
????????return?name;
????}
????
????public?String?toString()?{
????????return?name;
????}
}
?
區(qū)別在于toString()方法沒有被final修飾,可以重寫,name()方法不能重寫。
compareTo()方法
因為枚舉類實現了Comparable接口,所以必須重寫compareTo()方法,比較的是枚舉的次序,也就是ordinal,源碼如下:
public?final?int?compareTo(E?o)?{
????Enum<?>?other?=?(Enum<?>)o;
????Enum<E>?self?=?this;
????if?(self.getClass()?!=?other.getClass()?&&?//?optimization
????????self.getDeclaringClass()?!=?other.getDeclaringClass())
????????throw?new?ClassCastException();
????return?self.ordinal?-?other.ordinal;
}
因為實現Comparable接口,所以可以用來排序,比如這樣:
public?static?void?main(String[]?args)?throws?Exception?{
????//這里是亂序的枚舉數組
????Season[]?seasons?=?new?Season[]{Season.WINTER,?Season.AUTUMN,?Season.SPRING,?Season.SUMMER};
????//調用sort方法排序,按默認次序排序
????Arrays.sort(seasons);
????for?(Season?season?:?seasons)?{
????????System.out.println(season);
????}
}
輸出結果,按照默認次序排序:
SPRING
SUMMER
AUTUMN
WINTER
原理以枚舉Season為例,分析一下枚舉的底層。表面上看,一個枚舉很簡單:
public?enum?Season?{
????//春
????SPRING,
????//夏
????SUMMER,
????//秋
????AUTUMN,
????//冬
????WINTER;
}
實際上編譯器在編譯的時候做了很多動作,我們使用javap -v對Season.class文件反編譯,可以看到很多細節(jié)。
首先我們看到枚舉是繼承了抽象類Enum的類。
Season?extends?java.lang.Enum<Season>
第二,通過一段靜態(tài)代碼塊初始化枚舉。
??static?{};
????descriptor:?()V
????flags:?ACC_STATIC
????Code:
??????stack=4,?locals=0,?args_size=0
?????????0:?new???????????#4??????????????????//?class?io/github/yehongzhi/user/redisLock/Season
?????????3:?dup
?????????4:?ldc???????????#7??????????????????//?String?SPRING
?????????6:?iconst_0
?????????7:?invokespecial?#8??????????????????//?Method?"<init>":(Ljava/lang/String;I)V
????????10:?putstatic?????#9??????????????????//?Field?SPRING:Lio/github/yehongzhi/user/redisLock/Season;
????????13:?new???????????#4??????????????????//?class?io/github/yehongzhi/user/redisLock/Season
????????16:?dup
????????17:?ldc???????????#10?????????????????//?String?SUMMER
????????19:?iconst_1
????????20:?invokespecial?#8??????????????????//?Method?"<init>":(Ljava/lang/String;I)V
????????23:?putstatic?????#11?????????????????//?Field?SUMMER:Lio/github/yehongzhi/user/redisLock/Season;
????????26:?new???????????#4??????????????????//?class?io/github/yehongzhi/user/redisLock/Season
????????29:?dup
????????30:?ldc???????????#12?????????????????//?String?AUTUMN
????????32:?iconst_2
????????33:?invokespecial?#8??????????????????//?Method?"<init>":(Ljava/lang/String;I)V
????????36:?putstatic?????#13?????????????????//?Field?AUTUMN:Lio/github/yehongzhi/user/redisLock/Season;
????????39:?new???????????#4??????????????????//?class?io/github/yehongzhi/user/redisLock/Season
????????42:?dup
????????43:?ldc???????????#14?????????????????//?String?WINTER
????????45:?iconst_3
????????46:?invokespecial?#8??????????????????//?Method?"<init>":(Ljava/lang/String;I)V
????????49:?putstatic?????#15?????????????????//?Field?WINTER:Lio/github/yehongzhi/user/redisLock/Season;
????????52:?iconst_4
????????53:?anewarray?????#4??????????????????//?class?io/github/yehongzhi/user/redisLock/Season
????????56:?dup
????????57:?iconst_0
????????58:?getstatic?????#9??????????????????//?Field?SPRING:Lio/github/yehongzhi/user/redisLock/Season;
????????61:?aastore
????????62:?dup
????????63:?iconst_1
????????64:?getstatic?????#11?????????????????//?Field?SUMMER:Lio/github/yehongzhi/user/redisLock/Season;
????????67:?aastore
????????68:?dup
????????69:?iconst_2
????????70:?getstatic?????#13?????????????????//?Field?AUTUMN:Lio/github/yehongzhi/user/redisLock/Season;
????????73:?aastore
????????74:?dup
????????75:?iconst_3
????????76:?getstatic?????#15?????????????????//?Field?WINTER:Lio/github/yehongzhi/user/redisLock/Season;
????????79:?aastore
????????80:?putstatic?????#1??????????????????//?Field?$VALUES:[Lio/github/yehongzhi/user/redisLock/Season;
????????83:?return
這段靜態(tài)代碼塊的作用就是生成四個靜態(tài)常量字段的值,還生成了$VALUES字段,用于保存枚舉類定義的枚舉常量。相當于執(zhí)行了以下代碼:
Season?SPRING?=?new?Season1();
Season?SUMMER?=?new?Season2();
Season?AUTUMN?=?new?Season3();
Season?WINTER?=?new?Season4();
Season[]?$VALUES?=?new?Season[4];
$VALUES[0]?=?SPRING;
$VALUES[1]?=?SUMMER;
$VALUES[2]?=?AUTUMN;
$VALUES[3]?=?WINTER;
第三個,關于values()方法,這是一個靜態(tài)方法,作用是返回該枚舉類的數組,底層實現原理,其實是這樣的。
public?static?io.github.yehongzhi.user.redisLock.Season[]?values();
????Code:
???????0:?getstatic?????#1??????????????????//?Field?$VALUES:[Lio/github/yehongzhi/user/redisLock/Season;
???????3:?invokevirtual?#2??????????????????//?Method?"[Lio/github/yehongzhi/user/redisLock/Season;".clone:()Ljava/lang/Object;
???????6:?checkcast?????#3??????????????????//?class?"[Lio/github/yehongzhi/user/redisLock/Season;"
???????9:?areturn
其實是將靜態(tài)代碼塊初始化的$VALUES數組克隆一份,然后強轉成Season[]返回。相當于這樣:
public?static?Season[]?values(){
?return?(Season[])$VALUES.clone();
}
所以表面上,只是加了一個enum關鍵字定義枚舉,但是底層一旦確認是枚舉類,則會由編譯器對枚舉類進行特殊處理,通過靜態(tài)代碼塊初始化枚舉,只要是枚舉就一定會提供values()方法。
通過反編譯我們也知道所有的枚舉父類都是抽象類Enum,所以Enum有的成員變量,實現的接口,子類也會有。
所以只要是枚舉都會有name,ordinal這兩個字段,并且我們看Enum的構造器。
/**
*?Sole?constructor.??Programmers?cannot?invoke?this?constructor.
*?It?is?for?use?by?code?emitted?by?the?compiler?in?response?to
*?enum?type?declarations.
*/
protected?Enum(String?name,?int?ordinal)?{
????this.name?=?name;
????this.ordinal?=?ordinal;
}
翻譯一下上面那段英文,意思大概是:唯一的構造器,程序員沒法調用此構造器,它是供編譯器響應枚舉類型聲明而使用的。得出結論,枚舉實例的創(chuàng)建也是由編譯器完成的。
枚舉實現單例很多人都說,枚舉類是最好的實現單例的一種方式,因為枚舉類的單例是線程安全,并且是唯一一種不會被破壞的單例模式實現。也就是不能通過反射的方式創(chuàng)建實例,保證了整個應用中只有一個實例,非常硬核的單例。
public?class?SingletonObj?{
????//內部類使用枚舉
????private?enum?SingletonEnum?{
????????INSTANCE;
????????private?SingletonObj?singletonObj;
??????//在枚舉類的構造器里初始化singletonObj
????????SingletonEnum()?{
????????????singletonObj?=?new?SingletonObj();
????????}
????????private?SingletonObj?getSingletonObj()?{
????????????return?singletonObj;
????????}
????}
????//對外部提供的獲取單例的方法
????public?static?SingletonObj?getInstance()?{
????????//獲取單例對象,返回
????????return?SingletonEnum.INSTANCE.getSingletonObj();
????}
????//測試
????public?static?void?main(String[]?args)?{
????????SingletonObj?a?=?SingletonObj.getInstance();
????????SingletonObj?b?=?SingletonObj.getInstance();
????????System.out.println(a?==?b);//true
????}
}
假如有人想通過反射創(chuàng)建枚舉類呢,我們以Season枚舉為例。
public?static?void?main(String[]?args)?throws?Exception?{
????Constructor<Season>?constructor?=?Season.class.getDeclaredConstructor(String.class,?int.class);
????constructor.setAccessible(true);
????//通過反射調用構造器,創(chuàng)建枚舉
????Season?season?=?constructor.newInstance("NEW_SPRING",?4);
????System.out.println(season);
}
然后就會報錯,因為不允許對枚舉的構造器使用反射調用。

查看源碼,就可以看到,有個專門針對枚舉的if判斷。
public?T?newInstance(Object?...?initargs)?throws?InstantiationException,?IllegalAccessException,IllegalArgumentException,?InvocationTargetException?{
????if?(!override)?{
????????if?(!Reflection.quickCheckMemberAccess(clazz,?modifiers))?{
????????????Class<?>?caller?=?Reflection.getCallerClass();
????????????checkAccess(caller,?clazz,?null,?modifiers);
????????}
????}
????//判斷是否是枚舉,如果是枚舉的話,報、拋出異常
????if?((clazz.getModifiers()?&?Modifier.ENUM)?!=?0)
????????//拋出異常,不能通過反射創(chuàng)建枚舉
????????throw?new?IllegalArgumentException("Cannot?reflectively?create?enum?objects");
????ConstructorAccessor?ca?=?constructorAccessor;???//?read?volatile
????if?(ca?==?null)?{
????????ca?=?acquireConstructorAccessor();
????}
????@SuppressWarnings("unchecked")
????T?inst?=?(T)?ca.newInstance(initargs);
????return?inst;
}
總結枚舉看起來好像是很小一部分的知識,其實深入挖掘的話,我們會發(fā)現還是有很多地方值得學習的。第一點使用枚舉定義常量更容易擴展,而且代碼可讀性更強,維護性更好。接著第二點是需要了解枚舉自帶的方法。第三點通過反編譯,探索編譯器在編譯階段為枚舉做了什么事情。最后再講一下枚舉實現單例模式的例子。
這篇文章講到這里了,感謝大家的閱讀,希望看完這篇文章能有所收獲!
覺得有用就點個贊吧,你的點贊是我創(chuàng)作的最大動力~
我是一個努力讓大家記住的程序員。我們下期再見!!!
能力有限,如果有什么錯誤或者不當之處,請大家批評指正,一起學習交流!
