夯實Java基礎系列12:深入理解Java中的反射機制
本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視
https://github.com/h2pl/Java-Tutorial
喜歡的話麻煩點下Star哈
文章首發於我的個人部落格:
www.how2playlife.com
列舉(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/48087247
Java從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/Day$1.class、com/mikan/Day$2.class、com/mikan/Day$3.class、com/mikan/Day$4.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中推薦使用列舉來實現單例。
列舉類實戰
實戰一無參
(1)定義一個無參列舉類
enum SeasonType {
SPRING, SUMMER, AUTUMN, WINTER
}
(2)實戰中的使用
// 根據實際情況選擇下面的用法即可
SeasonType springType = SeasonType.SPRING; // 輸出 SPRING
String springString = SeasonType.SPRING.toString(); // 輸出 SPRING
實戰二有一參
(1)定義只有一個引數的列舉類
enum SeasonType {
// 通過建構函式傳遞引數並建立例項
SPRING("spring"),
SUMMER("summer"),
AUTUMN("autumn"),
WINTER("winter");
// 定義例項對應的引數
private String msg;
// 必寫:通過此構造器給列舉值建立例項
SeasonType(String msg) {
this.msg = msg;
}
// 通過此方法可以獲取到對應例項的引數值
public String getMsg() {
return msg;
}
}
(2)實戰中的使用
// 當我們為某個例項類賦值的時候可使用如下方式
String msg = SeasonType.SPRING.getMsg(); // 輸出 spring
實戰三有兩參
(1)定義有兩個引數的列舉類
public enum Season {
// 通過建構函式傳遞引數並建立例項
SPRING(1, "spring"),
SUMMER(2, "summer"),
AUTUMN(3, "autumn"),
WINTER(4, "winter");
// 定義例項對應的引數
private Integer key;
private String msg;
// 必寫:通過此構造器給列舉值建立例項
Season(Integer key, String msg) {
this.key = key;
this.msg = msg;
}
// 很多情況,我們可能從前端拿到的值是列舉類的 key ,然後就可以通過以下靜態方法獲取到對應列舉值
public static Season valueofKey(Integer key) {
for (Season season : Season.values()) {
if (season.key.equals(key)) {
return season;
}
}
throw new IllegalArgumentException("No element matches " + key);
}
// 通過此方法可以獲取到對應例項的 key 值
public Integer getKey() {
return key;
}
// 通過此方法可以獲取到對應例項的 msg 值
public String getMsg() {
return msg;
}
}
(2)實戰中的使用
// 輸出 key 為 1 的列舉值例項
Season season = Season.valueofKey(1);
// 輸出 SPRING 例項對應的 key
Integer key = Season.SPRING.getKey();
// 輸出 SPRING 例項對應的 msg
String msg = Season.SPRING.getMsg();
列舉類總結
其實列舉類懂了其概念後,列舉就變得相當簡單了,隨手就可以寫一個列舉類出來。所以如上幾個實戰小例子一定要先搞清楚概念,然後在練習幾遍就 ok 了。
重要的概念,我在這裡在贅述一遍,幫助老鐵們快速掌握這塊知識,首先記住,列舉類中的列舉值可以沒有引數,也可以有多個引數,每一個列舉值都是一個例項;
並且還有一點很重要,就是如果列舉值有 n 個引數,那麼建構函式中的引數值肯定有 n 個,因為宣告的每一個列舉值都會呼叫建構函式去建立例項,所以引數一定是一一對應的;既然明白了這一點,那麼我們只需要在列舉類中把這 n 個引數定義為 n 個成員變數,然後提供對應的 get() 方法,之後通過例項就可以隨意的獲取例項中的任意引數值了。
如果想讓列舉類更加的好用,就可以模仿我在實戰三中的寫法那樣,通過某一個引數值,比如 key 引數值,就能獲取到其對應的列舉值,然後想要什麼值,就 get 什麼值就好了。
列舉 API
我們使用 enum 定義的列舉類都是繼承 java.lang.Enum 類的,那麼就會繼承其 API ,常用的 API 如下:
- String name()
獲取列舉名稱
- int ordinal()
獲取列舉的位置(下標,初始值為 0 )
- valueof(String msg)
通過 msg 獲取其對應的列舉型別。(比如實戰二中的列舉類或其它列舉類都行,只要使用得當都可以使用此方法)
- values()
獲取列舉類中的所有列舉值(比如在實戰三中就使用到了)
總結
列舉本質上是通過普通的類來實現的,只是編譯器為我們進行了處理。每個列舉型別都繼承自java.lang.Enum,並自動添加了values和valueOf方法。
而每個列舉常量是一個靜態常量欄位,使用內部類實現,該內部類繼承了列舉類。所有列舉常量都通過靜態程式碼塊來進行初始化,即在類載入期間就初始化。
另外通過把clone、readObject、writeObject這三個方法定義為final的,同時實現是丟擲相應的異常。這樣保證了每個列舉型別及列舉常量都是不可變的。可以利用列舉的這兩個特性來實現執行緒安全的單例。
參考文章
https://blog.csdn.net/qq_34988624/article/details/86592229
https://www.meiwen.com.cn/subject/slhvhqtx.html
https://blog.csdn.net/qq_34988624/article/details/86592229
https://segmentfault.com/a/1190000012220863
https://my.oschina.net/wuxinshui/blog/1511484
https://blog.csdn.net/hukailee/article/details/81107412
微信公眾號
Java技術江湖
如果大家想要實時關注我更新的文章以及分享的乾貨的話,可以關注我的公眾號【Java技術江湖】一位阿里 Java 工程師的技術小站,作者黃小斜,專注 Java 相關技術:SSM、SpringBoot、MySQL、分散式、中介軟體、叢集、Linux、網路、多執行緒,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!
Java工程師必備學習資源: 一些Java工程師常用學習資源,關注公眾號後,後臺回覆關鍵字 “Java” 即可免費無套路獲取。
個人公眾號:黃小斜
作者是 985 碩士,螞蟻金服 JAVA 工程師,專注於 JAVA 後端技術棧:SpringBoot、MySQL、分散式、中介軟體、微服務,同時也懂點投資理財,偶爾講點演算法和計算機理論基礎,堅持學習和寫作,相信終身學習的力量!
程式設計師3T技術學習資源: 一些程式設計師學習技術的資源大禮包,關注公眾號後,後臺回覆關鍵字 “資料” 即可免費無套路獲取。