點贊再看,養成習慣,微信搜尋【三太子敖丙】關注這個網際網路苟且偷生的工具人。
本文 GitHub https://github.com/JavaFamily 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。
Java基礎:枚舉的用法與原理
在學習過程中,我們也隻是在定義常量的時候,會意識到枚舉的存在,而定義常量其實可以在類中實作,這時就會感覺枚舉有點雞肋。但在實際項目開發的過程中,枚舉因相當迷人的特性而受到越來越多的關注。
本文将按以下小節點來,一一介紹枚舉:
- 枚舉的實作
- 枚舉的用法
- 枚舉的原理
- 枚舉與單例
1. 枚舉的實作
枚舉是JDK1.5之後的特性,在此之前一般是在類中對常量進行定義。那麼為什麼需要枚舉呢?舉個栗子:
使用靜态變量定義四季
假如我們需要使用四個變量來代表“春夏秋冬”:
這時候隻要直接引用Season.SPRING就可以了,我們不需要去操心SPRING在存儲時是什麼資料。但是如果我們想做更多的事:知道下一個季節是什麼,還想把季節列印出來:
因為将Season類的構造方法私有化,外界就不能建立該類的對象了,這就避免了其他奇怪的季節的出現,所有Season對象都在該内部建立。
但是有個問題,用于存儲的int值不見了,是以我們還需要設定另一個方法:
這時如果需要一個Season對象對應的int資料,隻需要Season.toInt(Season.SPRING)即可。
但是這種寫法有一個隐患:如果想要擴充功能,需要寫大量的if-else判斷。
這時,枚舉來啦。
枚舉定義四季
我們還是以四季作為栗子:
好啦,枚舉定義完了。我們來看看怎麼使用它:
在枚舉中,預設的toString()方法傳回的就是枚舉類中對應的名稱。但是我們上面要求列印出來的是如”春季“等,而不是名稱本身,且四季對應的int值也是必要的。是以我們還得自己完善枚舉:
這樣,我們就實作了既定的目标,和之前的代碼相比,沒有那麼多if-else,是不是感覺少了很多煩惱呢?
是以,我們在定義有限的序列時,如星期、性别等,一般會通過靜态變量的形式進行定義,但是這種形式在添加功能的時候,就會需要很多不利于擴充和維護的代碼,是以枚舉的實作,可以簡化這些操作。
2. 枚舉的用法
枚舉類中有些方法還是比較常用的,在此示範幾個比較重要的方法。以四季為例:
Season.valueOf()方法
此方法的作用是傳來一個字元串,然後将它轉換成對應的枚舉變量。前提是傳入的字元串和定義枚舉變量的字元串一模一樣,須區分大小寫。如果傳入了一個不存在的字元串,那麼會抛出異常。
運作結果為:
Season.values()方法和Season.ordinal()方法
Season.values()方法會傳回包括所有枚舉變量的資料。
預設情況下,枚舉會給所有的枚舉變量提供一個預設的次序,該次序類似數組的下标,從0開始,而Season.ordinal()方法正是可以擷取其次序的方法。
for (Season s: Season.values()){
System.out.println(s + ".ordinal() --> "+s.ordinal());
}
運作結果為:
Season.toString()方法和Season.name()方法
Season.toString()方法會傳回枚舉定義枚舉變量時的字元串。此方法同Season.name()方法是一樣的。
運作結果為:
從實作過程來看,name()方法和toString()方法可以說是一樣的。
但它們之間唯一的差別是,toString()方法可以重寫,但name()方法被final修飾了,不能重寫。
Season.compareTo()方法
這個方法用于比較兩個枚舉變量的“大小”,實際上比較的是兩個枚舉變量之間的次序,并傳回次序相減之後的結果。
運作結果為:
我們來看看它的源碼:
在這裡其實我們就已經可以看到了,compareTo()方法中會先判斷是否屬于同一個枚舉的變量,然後再傳回內插補點。
那麼枚舉有什麼要注意的東西呢?
- 枚舉使用的是enum關鍵字,而不是class;
- 枚舉變量之間用逗号隔開,且枚舉變量最好用大寫,多個單詞之間使用“_"隔開(INT_SUM)。
- 定義完變量之後,以分号結束,如果隻是有枚舉變量,而不是自定義變量,分号可以省略。
- 隻需要類名.變量名就可以召喚枚舉變量了,跟使用靜态變量一樣。
枚舉與switch
枚舉是JDK1.5才有的特性,同時switch也更新了。使用switch進行條件判斷的時候,條件整數一般隻能是整型,字元型,而枚舉型确實也被switch所支援。還是用“四季“舉個栗子:
運作結果為:
枚舉的進階使用方法
我們還是拿四季來做個例子:
在這裡,SPRING對應的ordinal值對應的就是0,SUMMER對應的就是1。如果我們想将SPRING的值為1,那麼就需要自己定義變量:
如果我們想對一個枚舉變量做兩個次元的描述呢?
總結一下,如果需要自定義枚舉變量,需要注意一下幾點:
- 一定要把枚舉變量的定義放在第一行,并且以分号結尾;
- 構造函數必須私有化,但也不是一定要寫private,事實上枚舉的構造函數預設并強制為private,寫public是無法通過編譯的。
- ordinal還是按照它的規則給每個枚舉變量按次序指派,自定義變量與預設的ordinal屬性并不沖突。
3. 枚舉的原理
我們還是拿“四季”作為栗子:
反編譯之後,我們可以看到:
經過編譯器編譯之後,Season是一個繼承了Enum類的抽象類,而且枚舉中定義的枚舉變量變成了相應的public static final屬性,其類型為抽象類Season類型,名字就是枚舉變量的名字。
同時我們可以看到,Season.class的相同路徑下看到四個内部類的.class檔案:

也就是說,這四個枚舉常量分别使用了内部類來實作。
同時還添加了兩個方法values()和valueOf(String s)。我們使用的是預設的無參構造函數,但現在的構造函數有兩個參數。還生成了一個靜态代碼塊。下面我們來詳細看下是怎麼回事兒:
下面分析一下位元組碼中各部分内容,先拿靜态代碼塊下手:
靜态代碼塊
靜态代碼塊部分做的工作,就是分别設定生成的四個公共靜态常量字段的值,同時編譯器還生成一個靜态字段$VALUES,儲存的是枚舉類型定義的所有枚舉常量。相當于以下代碼:
values()方法
接下來我們來看看編譯器為我們生成的values()方法:
values()方法是一個公共的靜态方法,是以我們可以直接調用該方法,傳回枚舉的數組。而這個方法實作的是,将靜态代碼塊中初始化的$VALUES字段的值克隆出來,并且強制轉換成Season[]類型傳回,就相當于以下代碼:
valueOf()方法
接下來我們來看另一個由編譯器生成的valueOf()方法:
valueOf()也是一個公共的靜态方法,是以可以直接調用這個方法并傳回參數字元串表示的枚舉變量,另外,這個方法的實作是調用Enum.valueOf()方法,并把類型強制轉換為Season,它相當于如下的代碼:
最後,我們來看下編譯器生成的内部類是什麼樣的。
内部類
我們以Season$1為例:
可以看到,Season 1的構造函數有兩個入參呢?
關于這個問題,我們還是得從Season的父類Enum說起。
從Enum中我們可以看到,每個枚舉都定義了兩個屬性,name和ordinal,name表示枚舉變量的名稱,而ordinal則是根據變量定義的順序授予的整型值,從0開始。
在枚舉變量初始化的時候,會自動初始化這兩個字段,設定相應的值,是以會在Season()的構造方法中添加兩個參數。
而且我們可以從Enum的源碼中看到,大部分的方法都是final修飾的,特别是clone、readObject、writeObject這三個方法,保證了枚舉類型的不可變性,不能通過克隆、序列化和反序列化複制枚舉,這就保證了枚舉變量隻是一個執行個體,即是單例的。
總結一下,其實枚舉本質上也是通過普通的類來實作的,隻是編譯器為我們進行了處理。每個枚舉類型都繼承自Enum類,并由編譯器自動添加了values()和valueOf()方法,每個枚舉變量是一個靜态常量字段,由内部類實作,而這個内部類繼承了此枚舉類。
所有的枚舉變量都是通過靜态代碼塊進行初始化,也就是說在類加載期間就實作了。
另外,通過把clone、readObject、writeObject這三個方法定義為final,保證了每個枚舉類型及枚舉常量都是不可變的,也就是說,可以用枚舉實作線程安全的單例。
4. 枚舉與單例
枚舉類實作單例模式相當硬核,因為枚舉類型是線程安全的,且隻會裝載一次。使用枚舉類來實作單例模式,是所有的單例實作中唯一一種不會被破壞的單例模式實作。
結語
在學習Java枚舉類的時候,原本列出來了很多問題如Java枚舉的線程安全和序列化問題,但是在了解完Java枚舉的原理之後,這些問題,都迎刃而解了,也許在未來可能會碰上枚舉的特例吧。
本文主要對final關鍵字進行介紹,如果本文對你有幫助,請給一個贊吧,這會是我最大的動力~
我是敖丙,一個在網際網路苟且偷生的工具人。
你知道的越多,你不知道的越多,人才們的 【三連】 就是丙丙創作的最大動力,我們下期見!
注:如果本篇部落格有任何錯誤和建議,歡迎人才們留言,你快說句話啊!
文章持續更新,可以微信搜尋「 三太子敖丙 」第一時間閱讀,回複【資料】有我準備的一線大廠面試資料和履歷模闆,本文 GitHub https://github.com/JavaFamily 已經收錄,有大廠面試完整考點,歡迎Star。