版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。 https://blog.csdn.net/a724888/article/details/80173457
這位大俠,這是我的公衆号:程式員江湖。
分享程式員面試與技術的那些事。 幹貨滿滿,關注就送。
本文介紹了枚舉類的基本概念,使用方法,以及底層實作原理。幫助你更好地使用枚舉類并且了解枚舉類的内部實作細節。
具體代碼在我的GitHub中可以找到
https://github.com/h2pl/MyTech
喜歡的話麻煩點一下星哈謝謝。
文章首發于我的個人部落格:
https://h2pl.github.io/2018/05/02/javase15
更多關于Java後端學習的内容請到我的CSDN部落格上檢視:
https://blog.csdn.net/a724888
枚舉(enum)類型是Java 5新增的特性,它是一種新的類型,允許用常量來表示特定的資料片斷,而且全部都以類型安全的形式來表示。
## 初探枚舉類
在程式設計中,有時會用到由若幹個有限資料元素組成的集合,如一周内的星期一到星期日七個資料元素組成的集合,由三種顔色紅、黃、綠組成的集合,一個工作班組内十個職工組成的集合等等,程式中某個變量取值僅限于集合中的元素。此時,可将這些資料集合定義為枚舉類型。
是以,枚舉類型是某類資料可能取值的集合,如一周内星期可能取值的集合為:
{ Sun,Mon,Tue,Wed,Thu,Fri,Sat}
該集合可定義為描述星期的枚舉類型,該枚舉類型共有七個元素,因而用枚舉類型定義的枚舉變量隻能取集合中的某一進制素值。由于枚舉類型是導出資料類型,是以,必須先定義枚舉類型,然後再用枚舉類型定義枚舉型變量。
enum <枚舉類型名>
{ <枚舉元素表> };
其中:關鍵詞enum表示定義的是枚舉類型,枚舉類型名由辨別符組成,而枚舉元素表由枚舉元素或枚舉常量組成。例如:
enum weekdays
{ Sun,Mon,Tue,Wed,Thu,Fri,Sat };
定義了一個名為 weekdays的枚舉類型,它包含七個元素:Sun、Mon、Tue、Wed、Thu、Fri、Sat。
在編譯器編譯程式時,給枚舉類型中的每一個元素指定一個整型常量值(也稱為序号值)。若枚舉類型定義中沒有指定元素的整型常量值,則整型常量值從0開始依次遞增,是以,weekdays枚舉類型的七個元素Sun、Mon、Tue、Wed、Thu、Fri、Sat對應的整型常量值分别為0、1、2、3、4、5、6。
注意:在定義枚舉類型時,也可指定元素對應的整型常量值。
例如,描述邏輯值集合{TRUE、FALSE}的枚舉類型boolean可定義如下:
enum boolean
{ TRUE=1 ,FALSE=0 };
該定義規定:TRUE的值為1,而FALSE的值為0。
而描述顔色集合{red,blue,green,black,white,yellow}的枚舉類型colors可定義如下:
enum colors
{red=5,blue=1,green,black,white,yellow};
該定義規定red為5 ,blue為1,其後元素值從2 開始遞增加1。green、black、white、yellow的值依次為2、3、4、5。
此時,整數5将用于表示二種顔色red與yellow。通常兩個不同元素取相同的整數值是沒有意義的。枚舉類型的定義隻是定義了一個新的資料類型,隻有用枚舉類型定義枚舉變量才能使用這種資料類型。
## 枚舉類-文法
enum 與 class、interface 具有相同地位;
可以繼承多個接口;
可以擁有構造器、成員方法、成員變量;
1.2 枚舉類與普通類不同之處
預設繼承 java.lang.Enum 類,是以不能繼承其他父類;其中 java.lang.Enum 類實作了 java.lang.Serializable 和 java.lang.Comparable 接口;
使用 enum 定義,預設使用 final 修飾,是以不能派生子類;
構造器預設使用 private 修飾,且隻能使用 private 修飾;
枚舉類所有執行個體必須在第一行給出,預設添加 public static final 修飾,否則無法産生執行個體;
枚舉類的具體使用
這部分内容參考
https://blog.csdn.net/qq_27093465/article/details/52180865常量
public class 常量 {
}
enum Color {
Red, Green, Blue, Yellow
}
switch
JDK1.6之前的switch語句隻支援int,char,enum類型,使用枚舉,能讓我們的代碼可讀性更強。
public static void showColor(Color color) {
switch (color) {
case Red:
System.out.println(color);
break;
case Blue:
System.out.println(color);
break;
case Yellow:
System.out.println(color);
break;
case Green:
System.out.println(color);
break;
}
}
向枚舉中添加新方法
如果打算自定義自己的方法,那麼必須在enum執行個體序列的最後添加一個分号。而且 Java 要求必須先定義 enum 執行個體。
enum Color {
//每個顔色都是枚舉類的一個執行個體,并且構造方法要和枚舉類的格式相符合。
//如果執行個體後面有其他内容,執行個體序列結束時要加分号。
Red("紅色", 1), Green("綠色", 2), Blue("藍色", 3), Yellow("黃色", 4);
String name;
int index;
Color(String name, int index) {
this.name = name;
this.index = index;
}
public void showAllColors() {
//values是Color執行個體的數組,在通過index和name可以擷取對應的值。
for (Color color : Color.values()) {
System.out.println(color.index + ":" + color.name);
}
}
}
覆寫枚舉的方法
所有枚舉類都繼承自Enum類,是以可以重寫該類的方法
下面給出一個toString()方法覆寫的例子。
@Override
public String toString() {
return this.index + ":" + this.name;
}
實作接口
所有的枚舉都繼承自java.lang.Enum類。由于Java 不支援多繼承,是以枚舉對象不能再繼承其他類。
enum Color implements Print{
@Override
public void print() {
System.out.println(this.name);
}
}
使用接口組織枚舉
搞個實作接口,來組織枚舉,簡單講,就是分類吧。如果大量使用枚舉的話,這麼幹,在寫代碼的時候,就很友善調用啦。
public class 用接口組織枚舉 {
public static void main(String[] args) {
Food cf = chineseFood.dumpling;
Food jf = Food.JapaneseFood.fishpiece;
for (Food food : chineseFood.values()) {
System.out.println(food);
}
for (Food food : Food.JapaneseFood.values()) {
System.out.println(food);
}
}
}
interface Food {
enum JapaneseFood implements Food {
suse, fishpiece
}
}
enum chineseFood implements Food {
dumpling, tofu
}
枚舉類集合
java.util.EnumSet和java.util.EnumMap是兩個枚舉集合。EnumSet保證集合中的元素不重複;EnumMap中的 key是enum類型,而value則可以是任意類型。
EnumSet在JDK中沒有找到實作類,這裡寫一個EnumMap的例子
public class 枚舉類集合 {
public static void main(String[] args) {
EnumMap<Color, String> map = new EnumMap<Color, String>(Color.class);
map.put(Color.Blue, "Blue");
map.put(Color.Yellow, "Yellow");
map.put(Color.Red, "Red");
System.out.println(map.get(Color.Red));
}
}
使用枚舉類的注意事項
枚舉類型對象之間的值比較,是可以使用==,直接來比較值,是否相等的,不是必須使用equals方法的喲。
因為枚舉類Enum已經重寫了equals方法
/**
* Returns true if the specified object is equal to this
* enum constant.
*
* @param other the object to be compared for equality with this object.
* @return true if the specified object is equal to this
* enum constant.
*/
public final boolean equals(Object other) {
return this==other;
}
枚舉類的底層原理
這部分參考
https://blog.csdn.net/mhmyqn/article/details/48087247Java從JDK1.5開始支援枚舉,也就是說,Java一開始是不支援枚舉的,就像泛型一樣,都是JDK1.5才加入的新特性。通常一個特性如果在一開始沒有提供,在語言發展後期才添加,會遇到一個問題,就是向後相容性的問題。
像Java在1.5中引入的很多特性,為了向後相容,編譯器會幫我們寫的源代碼做很多事情,比如泛型為什麼會擦除類型,為什麼會生成橋接方法,foreach疊代,自動裝箱/拆箱等,這有個術語叫“文法糖”,而編譯器的特殊處理叫“解文法糖”。那麼像枚舉也是在JDK1.5中才引入的,又是怎麼實作的呢?
Java在1.5中添加了java.lang.Enum抽象類,它是所有枚舉類型基類。提供了一些基礎屬性和基礎方法。同時,對把枚舉用作Set和Map也提供了支援,即java.util.EnumSet和java.util.EnumMap。
接下來定義一個簡單的枚舉類
public enum Day {
MONDAY {
@Override
void say() {
System.out.println("MONDAY");
}
}
, TUESDAY {
@Override
void say() {
System.out.println("TUESDAY");
}
}, FRIDAY("work"){
@Override
void say() {
System.out.println("FRIDAY");
}
}, SUNDAY("free"){
@Override
void say() {
System.out.println("SUNDAY");
}
};
String work;
//沒有構造參數時,每個執行個體可以看做常量。
//使用構造參數時,每個執行個體都會變得不一樣,可以看做不同的類型,是以編譯後會生成執行個體個數對應的class。
private Day(String work) {
this.work = work;
}
private Day() {
}
//枚舉執行個體必須實作枚舉類中的抽象方法
abstract void say ();
}
反編譯結果
D:\MyTech\out\production\MyTech\com\javase\枚舉類>javap Day.class
Compiled from "Day.java"
public abstract class com.javase.枚舉類.Day extends java.lang.Enum<com.javase.枚舉類.Day> {
public static final com.javase.枚舉類.Day MONDAY;
public static final com.javase.枚舉類.Day TUESDAY;
public static final com.javase.枚舉類.Day FRIDAY;
public static final com.javase.枚舉類.Day SUNDAY;
java.lang.String work;
public static com.javase.枚舉類.Day[] values();
public static com.javase.枚舉類.Day valueOf(java.lang.String);
abstract void say();
com.javase.枚舉類.Day(java.lang.String, int, com.javase.枚舉類.Day$1);
com.javase.枚舉類.Day(java.lang.String, int, java.lang.String, com.javase.枚舉類.Day$1);
static {};
}
可以看到,一個枚舉在經過編譯器編譯過後,變成了一個抽象類,它繼承了java.lang.Enum;而枚舉中定義的枚舉常量,變成了相應的public static final屬性,而且其類型就抽象類的類型,名字就是枚舉常量的名字.
同時我們可以在Operator.class的相同路徑下看到四個内部類的.class檔案com/mikan/Day1.class、com/mikan/Day1.class、com/mikan/Day2.class、com/mikan/Day3.class、com/mikan/Day3.class、com/mikan/Day4.class,也就是說這四個命名字段分别使用了内部類來實作的;同時添加了兩個方法values()和valueOf(String);我們定義的構造方法本來隻有一個參數,但卻變成了三個參數;同時還生成了一個靜态代碼塊。這些具體的内容接下來仔細看看。
下面分析一下位元組碼中的各部分,其中:
InnerClasses:
static #23; //class com/javase/枚舉類/Day$4
static #18; //class com/javase/枚舉類/Day$3
static #14; //class com/javase/枚舉類/Day$2
static #10; //class com/javase/枚舉類/Day$1
從中可以看到它有4個内部類,這四個内部類的詳細資訊後面會分析。
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=5, locals=0, args_size=0
0: new #10 // class com/javase/枚舉類/Day$1
3: dup
4: ldc #11 // String MONDAY
6: iconst_0
7: invokespecial #12 // Method com/javase/枚舉類/Day$1."<init>":(Ljava/lang/String;I)V
10: putstatic #13 // Field MONDAY:Lcom/javase/枚舉類/Day;
13: new #14 // class com/javase/枚舉類/Day$2
16: dup
17: ldc #15 // String TUESDAY
19: iconst_1
20: invokespecial #16 // Method com/javase/枚舉類/Day$2."<init>":(Ljava/lang/String;I)V
//後面類似,這裡省略
}
其實編譯器生成的這個靜态代碼塊做了如下工作:分别設定生成的四個公共靜态常量字段的值,同時編譯器還生成了一個靜态字段$VALUES,儲存的是枚舉類型定義的所有枚舉常量
編譯器添加的values方法:
public static com.javase.Day[] values();
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #2 // Field $VALUES:[Lcom/javase/Day;
3: invokevirtual #3 // Method "[Lcom/mikan/Day;".clone:()Ljava/lang/Object;
6: checkcast #4 // class "[Lcom/javase/Day;"
9: areturn
這個方法是一個公共的靜态方法,是以我們可以直接調用該方法(Day.values()),傳回這個枚舉值的數組,另外,這個方法的實作是,克隆在靜态代碼塊中初始化的$VALUES字段的值,并把類型強轉成Day[]類型傳回。
造方法為什麼增加了兩個參數?
有一個問題,構造方法我們明明隻定義了一個參數,為什麼生成的構造方法是三個參數呢?
從Enum類中我們可以看到,為每個枚舉都定義了兩個屬性,name和ordinal,name表示我們定義的枚舉常量的名稱,如FRIDAY、TUESDAY,而ordinal是一個順序号,根據定義的順序分别賦予一個整形值,從0開始。在枚舉常量初始化時,會自動為初始化這兩個字段,設定相應的值,是以才在構造方法中添加了兩個參數。即:
另外三個枚舉常量生成的内部類基本上差不多,這裡就不重複說明了。
我們可以從Enum類的代碼中看到,定義的name和ordinal屬性都是final的,而且大部分方法也都是final的,特别是clone、readObject、writeObject這三個方法,這三個方法和枚舉通過靜态代碼塊來進行初始化一起。
它保證了枚舉類型的不可變性,不能通過克隆,不能通過序列化和反序列化來複制枚舉,這能保證一個枚舉常量隻是一個執行個體,即是單例的,是以在effective java中推薦使用枚舉來實作單例。
總結
枚舉本質上是通過普通的類來實作的,隻是編譯器為我們進行了處理。每個枚舉類型都繼承自java.lang.Enum,并自動添加了values和valueOf方法。
而每個枚舉常量是一個靜态常量字段,使用内部類實作,該内部類繼承了枚舉類。所有枚舉常量都通過靜态代碼塊來進行初始化,即在類加載期間就初始化。
另外通過把clone、readObject、writeObject這三個方法定義為final的,同時實作是抛出相應的異常。這樣保證了每個枚舉類型及枚舉常量都是不可變的。可以利用枚舉的這兩個特性來實作線程安全的單例。