Java 枚舉
為什么要引入枚舉類
一個小案例
你寫了一個小程序,不過好久不用了,突然有一天,你想使用一下它。程序要想正確運行,需要將今天星期幾存到數據庫里。這個時候,你開始犯難了。
當初的你還很年輕,不懂程序界的險惡,設計這個程序的時候,傻不拉幾把這個字段設計為int類型的,用0代表周日,1代表周一。。。6代表周六,添加的時候就setWeekday(0)。但是這么長時間沒用了,你忘記自己是從周一開始計算還是周日開始計算了,換句話說,你想不起來0代表的是周一還是周日了!
於是你各種翻代碼,看數據庫的字段,數據庫保存的信息,終於搞懂了,你很開心,用了一次之后,覺得這個程序沒意思,又不用了。
很久之后,你心血來潮,又想用一次它,很不幸,你又忘記到底0代表周一還是周日了,一番查找之后。你決定重構代碼,因為你受不了了!!
靜態變量來幫忙
經過一番思考,你決定使用七個靜態變量來代表星期幾,以后只要引用和靜態變量就可以了,而不用自己輸入012….你這么寫:
public class Weekday {
public final static int SUN = 0;
public final static int MON = 1;
public final static int TUE = 2;
public final static int WED = 3;
public final static int THU = 4;
public final static int FRI = 5;
public final static int SAT = 6;
}
機智如你,這個時候,只要Weekday.SUN
就可以了,不用操心到底應該填寫0還是填寫1。
但是這個時候的你,也不是當初初出茅廬的小伙子了,很明顯,這樣寫已經不能滿足你了。你還想讓這個類做更多的事,比如,你想知道下一天是星期幾,還想把今天是星期幾打印出來。一番深思熟慮后,你改成了這樣:
public class Weekday {
private Weekday(){}
public final static Weekday SUN = new Weekday();
public final static Weekday MON = new Weekday();
public final static Weekday TUE = new Weekday();
public final static Weekday WED = new Weekday();
public final static Weekday THU = new Weekday();
public final static Weekday FRI = new Weekday();
public final static Weekday SAT = new Weekday();
public static Weekday getNextDay(Weekday nowDay){
if(nowDay == SUN) {
return MON;
}else if(nowDay == MON) {
return TUE;
}else if(nowDay == TUE) {
return WED;
}else if(nowDay == WED) {
return THU;
}else if(nowDay == THU) {
return FRI;
}else if(nowDay == FRI) {
return SAT;
}else {
return SUN;
}
}
public static void printNowDay(Weekday nowDay){
if(nowDay == SUN)
System.out.println("sunday");
else if(nowDay == MON)
System.out.println("monday");
else if(nowDay == TUE)
System.out.println("tuesday");
else if(nowDay == WED)
System.out.println("wednesday");
else if(nowDay == THU)
System.out.println("thursday");
else if(nowDay == FRI)
System.out.println("friday");
else
System.out.println("saturday");
}
}
class Test1{
public static void main(String[] args) {
Weekday nowday = Weekday.SUN;
Weekday.printNowDay(nowday);
Weekday nextDay = Weekday.getNextDay(nowday);
System.out.print("nextday ====> ");
Weekday.printNowDay(nextDay);
}
}
//測試結果:
//sunday
//nextday ====> monday
喲,不錯。考慮的很詳細。並且私有構造方法后,外界就不能創建該類的對象了,這樣就避免了星期八星期九的出現,所有Weekday的對象都在該類內部創建。
不對,好像缺了點什么,我要的是int!我的int呢?!。所以,你還需要一個這樣的方法:
public static int toInt(Weekday nowDay){
if(nowDay == SUN)
return 0;
else if(nowDay == MON)
return 1;
else if(nowDay == TUE)
return 2;
else if(nowDay == WED)
return 3;
else if(nowDay == THU)
return 4;
else if(nowDay == FRI)
return 5;
else
return 6;
}
當你需要一個整形數據的時候,只需要Weekday.toInt(Weekday.SUN);
,看起來你好像完成了你的任務。
但是,你有沒有發現,這樣寫,好麻煩啊。如果想要擴展一下功能,大量的ifelse會讓人眼花繚亂。
有沒有更好的方式呢?你大概已經知道了,沒錯,我們需要枚舉類!
我們先來看看枚舉類是什么。
一個簡單的枚舉類
話不多說,先來代碼:
public enum Weekday {
SUN,MON,TUS,WED,THU,FRI,SAT
}
代碼這么少?
沒錯,這就是枚舉類,我們來看看怎么使用它:
class Test2{
public static void main(String[] args) {
Weekday sun = Weekday.SUN;
System.out.println(sun); // 輸出 SUN
}
}
看起來和上面的靜態變量使用方式差不多,而且默認的toString方法返回的就是對應的名字。
我們上面的那段代碼重寫toString也是不可以打印出當前是星期幾的,因為toString方法沒有參數。所以我們自己寫了一個printNowDay方法。
當然,這么簡單的枚舉類是不可能實現我們的要求的,所以,我們還要接着寫:
public enum Weekday {
SUN(0),MON(1),TUS(2),WED(3),THU(4),FRI(5),SAT(6);
private int value;
private Weekday(int value){
this.value = value;
}
public static Weekday getNextDay(Weekday nowDay){
int nextDayValue = nowDay.value;
if (++nextDayValue == 7){
nextDayValue =0;
}
return getWeekdayByValue(nextDayValue);
}
public static Weekday getWeekdayByValue(int value) {
for (Weekday c : Weekday.values()) {
if (c.value == value) {
return c;
}
}
return null;
}
}
class Test2{
public static void main(String[] args) {
System.out.println("nowday ====> " + Weekday.SAT);
System.out.println("nowday int ====> " + Weekday.SAT.ordinal());
System.out.println("nextday ====> " + Weekday.getNextDay(Weekday.SAT)); // 輸出 SUN
//輸出:
//nowday ====> SAT
//nowday int ====> 6
//nextday ====> SUN
}
}
這樣就完成了我們的目標,和之前的代碼比起來,有沒有覺得突然高大上了許多?沒有那么多煩人的ifelse,世界都清凈了。
好了,現在你大概知道為什么要引入枚舉類了吧?就是因為在沒有枚舉類的時候,我們要定義一個有限的序列,比如星期幾,男人女人,春夏秋冬,一般會通過上面那種靜態變量的形式,但是使用那樣的形式如果需要一些其他的功能,需要些很多奇奇怪怪的代碼。所以,枚舉類的出現,就是為了簡化這種操作。
可以將枚舉類理解為是java的一種語法糖。
枚舉類的用法
最簡單的使用
最簡單的枚舉類就像我們上面第一個定義的枚舉類一樣:
public enum Weekday {
SUN,MON,TUS,WED,THU,FRI,SAT
}
如何使用它呢?
先來看看它有哪些方法:
這是Weekday可以調用的方法和參數。發現它有兩個方法:value()和valueOf()。還有我們剛剛定義的七個變量。
這些事枚舉變量的方法。我們接下來會演示幾個比較重要的:
public enum Weekday {
SUN,MON,TUS,WED,THU,FRI,SAT
}
class Test3{
public static void main(String[] args) {
System.out.println(Weekday.valueOf("mon".toUpperCase()));
//MON
for (Weekday w : Weekday.values()){
System.out.println(w + ".ordinal() ====>" +w.ordinal());
}
//SUN.ordinal() ====>0
//MON.ordinal() ====>1
//TUS.ordinal() ====>2
//WED.ordinal() ====>3
//THU.ordinal() ====>4
//FRI.ordinal() ====>5
//SAT.ordinal() ====>6
System.out.println("Weekday.MON.compareTo(Weekday.FRI) ===> " + Weekday.MON.compareTo(Weekday.FRI));
System.out.println("Weekday.MON.compareTo(Weekday.MON) ===> " + Weekday.MON.compareTo(Weekday.MON));
System.out.println("Weekday.MON.compareTo(Weekday.SUM) ===> " + Weekday.MON.compareTo(Weekday.SUN));
//Weekday.MON.compareTo(Weekday.FRI) ===> -4
//Weekday.MON.compareTo(Weekday.MON) ===> 0
//Weekday.MON.compareTo(Weekday.SUM) ===> 1
System.out.println("Weekday.MON.name() ====> " + Weekday.MON.name());
//Weekday.MON.name() ====> MON
}
}
這段代碼,我們演示了幾個常用的方法和功能:
-
Weekday.valueOf() 方法:
它的作用是傳來一個字符串,然后將它轉變為對應的枚舉變量。前提是你傳的字符串和定義枚舉變量的字符串一抹一樣,區分大小寫。如果你傳了一個不存在的字符串,那么會拋出異常。
-
Weekday.values()方法。
這個方法會返回包括所有枚舉變量的數組。在該例中,返回的就是包含了七個星期的Weekday[]。可以方便的用來做循環。
-
枚舉變量的toString()方法。
該方法直接返回枚舉定義枚舉變量的字符串,比如MON就返回【”MON”】。
-
枚舉變量的.ordinal()方法。
默認請款下,枚舉類會給所有的枚舉變量一個默認的次序,該次序從0開始,類似於數組的下標。而.ordinal()方法就是獲取這個次序(或者說下標)
-
枚舉變量的compareTo()方法。
該方法用來比較兩個枚舉變量的”大小”,實際上比較的是兩個枚舉變量的次序,返回兩個次序相減后的結果,如果為負數,就證明變量1”小於”變量2 (變量1.compareTo(變量2),返回【變量1.ordinal() - 變量2.ordinal()】)
這是compareTo的源碼,會先判斷是不是同一個枚舉類的變量,然后再返回差值。
-
枚舉類的name()方法。
它和toString()方法的返回值一樣,事實上,這兩個方法本來就是一樣的:
這兩個方法的默認實現是一樣的,唯一的區別是,你可以重寫toString方法。name變量就是枚舉變量的字符串形式。
還有一些其他的方法我就暫時不介紹了,感興趣的話可以自己去看看文檔或者源碼,都挺簡單的。
要點:
使用的是enum關鍵字而不是class。
多個枚舉變量直接用逗號隔開。
枚舉變量最好大寫,多個單詞之間使用”_”隔開(比如:INT_SUM)。
定義完所有的變量后,以分號結束,如果只有枚舉變量,而沒有自定義變量,分號可以省略(例如上面的代碼就忽略了分號)。
在其他類中使用enum變量的時候,只需要【類名.變量名】就可以了,和使用靜態變量一樣。
但是這種簡單的使用顯然不能體現出枚舉的強大,我們來學習一下復雜的使用:
枚舉的高級使用方法
就像我們前面的案例一樣,你需要讓每一個星期幾對應到一個整數,比如星期天對應0。上面講到了,枚舉類在定義的時候會自動為每個變量添加一個順序,從0開始。
假如你希望0代表星期天,1代表周一。。。並且你在定義枚舉類的時候,順序也是這個順序,那你可以不用定義新的變量,就像這樣:
public enum Weekday {
SUN,MON,TUS,WED,THU,FRI,SAT
}
這個時候,星期天對應的ordinal值就是0,周一對應的就是1,滿足你的要求。但是,如果你這么寫,那就有問題了:
public enum Weekday {
MON,TUS,WED,THU,FRI,SAT,SUN
}
我吧SUN放到了最后,但是我還是希0代表SUN,1代表MON怎么辦呢?默認的ordinal是指望不上了,因為它只會傻傻的給第一個變量0,給第二個1。。。
所以,我們需要自己定義變量!
看代碼:
public enum Weekday {
MON(1),TUS(2),WED(3),THU(4),FRI(5),SAT(6),SUN(0);
private int value;
private Weekday(int value){
this.value = value;
}
}
我們對上面的代碼做了一些改變:
首先,我們在每個枚舉變量的后面加上了一個括號,里面是我們希望它代表的數字。
然后,我們定義了一個int變量,然后通過構造函數初始化這個變量。
你應該也清楚了,括號里的數字,其實就是我們定義的那個int變量。這句叫做自定義變量。
請注意:這里有三點需要注意:
一定要把枚舉變量的定義放在第一行,並且以分號結尾。
構造函數必須私有化。事實上,private是多余的,你完全沒有必要寫,因為它默認並強制是private,如果你要寫,也只能寫private,寫public是不能通過編譯的。
自定義變量與默認的ordinal屬性並不沖突,ordinal還是按照它的規則給每個枚舉變量按順序賦值。
好了,你很聰明,你已經掌握了上面的知識,你想,既然能自定義一個變量,能不能自定義兩個呢?
當然可以:
public enum Weekday {
MON(1,"mon"),TUS(2,"tus"),WED(3,"wed"),THU(4,"thu"),FRI(5,"fri"),SAT(6,"sat"),SUN(0,"sun");
private int value;
private String label;
private Weekday(int value,String label){
this.value = value;
this.label = label;
}
}
你可以定義任何你想要的變量。學完了這些,大概枚舉類你也應該掌握了,但是,還有沒有其他用法呢?
枚舉類中的抽象類
如果我在枚舉類中定義一個抽象方法會怎么樣?
你要知道,枚舉類不能繼承其他類,也不能被其他類繼承。至於為什么,我們后面會說到。
你應該知道,有抽象方法的類必然是抽象類,抽象類就需要子類繼承它然后實現它的抽象方法,但是呢,枚舉類不能被繼承。。你是不是有點亂?
我們先來看代碼:
public enum TrafficLamp {
RED(30) {
@Override
public TrafficLamp getNextLamp() {
return GREEN;
}
}, GREEN(45) {
@Override
public TrafficLamp getNextLamp() {
return YELLOW;
}
}, YELLOW(5) {
@Override
public TrafficLamp getNextLamp() {
return RED;
}
};
private int time;
private TrafficLamp(int time) {
this.time = time;
}
//一個抽象方法
public abstract TrafficLamp getNextLamp();
}
你好像懂了點什么。但是你好像又不太懂。為什么一個變量的后邊可以帶一個代碼塊並且實現抽象方法呢?
別着急,帶着這個疑問,我們來看一下枚舉類的實現原理。
枚舉類的實現原理
從最簡單的看起:
public enum Weekday {
SUN,MON,TUS,WED,THU,FRI,SAT
}
還是這段熟悉的代碼,我們編譯一下它,再反編譯一下看看它到底是什么樣子的:
你是不是覺得很熟悉?反編譯出來的代碼和我們一開始用靜態變量自己寫的那個類出奇的相似!
而且,你看到了熟悉的values()方法和valueOf()方法。
仔細看,這個類繼承了java.lang.Enum類!所以說,枚舉類不能再繼承其他類了,因為默認已經繼承了Enum類。
並且,這個類是final的!所以它不能被繼承!
回到我們剛才的那個疑問:
RED(30) {
@Override
public TrafficLamp getNextLamp() {
return GREEN;
}
}
為什么會有這么神奇的代碼?現在你差不多懂了。因為RED本身就是一個TrafficLamp對象的引用。實際上,在初始化這個枚舉類的時候,你可以理解為執行的是TrafficLamp RED = new TrafficLamp(30)
,但是因為TrafficLamp里面有抽象方法,還記得匿名內部類么?
我們可以這樣來創建一個TrafficLamp引用:
TrafficLamp RED = new TrafficLamp(30){
@Override
public TrafficLamp getNextLamp() {
return GREEN;
}
}
而在枚舉類中,我們只需要像上面那樣寫【RED(30){}
】就可以了,因為java會自動的去幫我們完成這一系列操作。
如果你還是不太理解,那么你可以自己去反編譯一下TrafficLamp這個類,看看jvm是怎么處理它的就明白了。
枚舉類的其他用法
說一說枚舉類的其他用法。
switch語句中使用
enum Signal {
GREEN, YELLOW, RED
}
public class TrafficLight {
Signal color = Signal.RED;
public void change() {
switch (color) {
case RED:
color = Signal.GREEN;
break;
case YELLOW:
color = Signal.RED;
break;
case GREEN:
color = Signal.YELLOW;
break;
}
}
}
實現接口
雖然枚舉類不能繼承其他類,但是還是可以實現接口的
public interface Behaviour {
void print();
String getInfo();
}
public enum Color implements Behaviour {
RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLO("黃色", 4);
// 成員變量
private String name;
private int index;
// 構造方法
private Color(String name, int index) {
this.name = name;
this.index = index;
}
// 接口方法
@Override
public String getInfo() {
return this.name;
}
// 接口方法
@Override
public void print() {
System.out.println(this.index + ":" + this.name);
}
}
使用接口組織枚舉
public interface Food {
enum Coffee implements Food {
BLACK_COFFEE, DECAF_COFFEE, LATTE, CAPPUCCINO
}
enum Dessert implements Food {
FRUIT, CAKE, GELATO
}
}
使用枚舉創建單例模式
*使用枚舉創建的單例模式:*
public enum EasySingleton{
INSTANCE;
}
代碼就這么簡單,你可以使用EasySingleton.INSTANCE調用它,比起你在單例中調用getInstance()方法容易多了。
我們來看看正常情況下是怎樣創建單例模式的:
*用雙檢索實現單例:*
下面的代碼是用雙檢索實現單例模式的例子,在這里getInstance()方法檢查了兩次來判斷INSTANCE是否為null,這就是為什么叫雙檢索的原因,記住雙檢索在java5之前是有問題的,但是java5在內存模型中有了volatile變量之后就沒問題了。
public class DoubleCheckedLockingSingleton{
private volatile DoubleCheckedLockingSingleton INSTANCE;
private DoubleCheckedLockingSingleton(){}
public DoubleCheckedLockingSingleton getInstance(){
if(INSTANCE == null){
synchronized(DoubleCheckedLockingSingleton.class){
//double checking Singleton instance
if(INSTANCE == null){
INSTANCE = new DoubleCheckedLockingSingleton();
}
}
}
return INSTANCE;
}
}
你可以訪問DoubleCheckedLockingSingleTon.getInstance()來獲得實例對象。
*用靜態工廠方法實現單例:*
public class Singleton{
private static final Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getSingleton(){
return INSTANCE;
}
}
你可以調用Singleton.getInstance()方法來獲得實例對象。
上面的兩種方式就是懶漢式和惡漢式單利的創建,但是無論哪一種,都不如枚舉來的方便。而且傳統的單例模式的另外一個問題是一旦你實現了serializable接口,他們就不再是單例的了。但是枚舉類的父類【Enum類】實現了Serializable接口,也就是說,所有的枚舉類都是可以實現序列化的,這也是一個優點。
總結
最后總結一下:
可以創建一個enum類,把它看做一個普通的類。除了它不能繼承其他類了。(java是單繼承,它已經繼承了Enum),可以添加其他方法,覆蓋它本身的方法
switch()參數可以使用enum
values()方法是編譯器插入到enum定義中的static方法,所以,當你將enum實例向上轉型為父類Enum是,values()就不可訪問了。解決辦法:在Class中有一個getEnumConstants()方法,所以即便Enum接口中沒有values()方法,我們仍然可以通過Class對象取得所有的enum實例
無法從enum繼承子類,如果需要擴展enum中的元素,在一個接口的內部,創建實現該接口的枚舉,以此將元素進行分組。達到將枚舉元素進行分組。
enum允許程序員為eunm實例編寫方法。所以可以為每個enum實例賦予各自不同的行為。
本文到這里就差不多結束了。可能舉得例子不是很恰當,代碼寫的不是很優雅,不過我只是用來引出枚舉的,大家不要雞蛋里頭挑骨頭哈哈。
如果文章內容有什么問題,請及時與我聯系。
除此之外,還有兩個枚舉集合:【java.util.EnumSet和java.util.EnumMap】沒有講。關於枚舉集合的使用會在后面講集合框架的時候再詳細講解。