最近在整理Java的知識點,這篇文章講解列舉的使用和原理,希望可以幫助到大家,也希望能得到大家的支持和關注,本文章原創,轉載請說明出處,
文章目錄
- 1. 為什么需要列舉
- 2. 列舉型別的特性和使用
- 2.1. 定義列舉
- 2.2. 列舉特性--定義實體變數、實體代碼塊、實體方法和構造方法
- 2.3. 列舉特性--定義靜態變數、靜態代碼塊以及靜態方法
- 2.4. 列舉特性--列舉不能繼承其他類或列舉,也不可以被繼承,但是可以實作介面
- 2.5. 列舉特性--可以用作switch陳述句
- 2.6. 列舉特性--enum不可以作為資料型別
- 2.7. 列舉特性--enum可以宣告抽象方法
- 2.8. 列舉的常用方法
- 2.9. 列舉的常用工具類別庫
- 3. 列舉的實作原理
- 4. 參考文章
1. 為什么需要列舉
在JDK1.5之前,我們要是想定義一些有關常量的內容,例如定義幾個常量,表示從周一到周末,一般都是在一個類,或者一個介面中,寫類似于如下代碼:
public class WeekDayConstant {
public static final int MONDAY = 0;
public static final int TUESDAY = 1;
public static final int WEDNESDAY = 2;
public static final int THURSDAY = 3;
//...
}
這樣做也可以實作功能,有幾個缺點:
- 各個常量的值可能會一樣,出現混淆,例如不小心把TUESDAY 定義為0
- 使用起來并不是很方便,例如想要獲取某一種列舉的所有列舉值串列,根名稱獲取值等,還要去編碼實作
- 并不是很安全,例如反射修改常量的值,MONDAY 的值可能被修改為1
- 方式并不是很優雅
為了不重復造輪子,Java組織在JDK1.5的時候,引入了列舉enum關鍵字(enum就是enumeration的縮寫),我們可以定義列舉型別,
2. 列舉型別的特性和使用
2.1. 定義列舉
定義的格式是:
訪問修飾符 enum 列舉型別名稱{
一個或多個列舉值定義,一般采用大寫加下劃線的方式,用英文逗號分隔,例如,
A,B,C;
在最后一個列舉值后面建議加一個分號,對于與只有列舉值的列舉定義來說,可以沒有分號
后面就是一些方法的定義
}
例如,周一到周末的列舉定義:
public enum WeekDay {
MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY;
}
可以單獨定義一個檔案,也可以定義在其他類的檔案中,
2.2. 列舉特性–定義實體變數、實體代碼塊、實體方法和構造方法
在列舉中可以定義自己的構造方法,有引數無引數都可以,例如
//定義功課的列舉,每一門功課都有一個老師,通過構造方法傳入
public enum Subject {
CHINESE("王老師"),
MATH("張老師"),
ENGLISH("Mr. Yang");
private String teacherName;
//列舉的構造方法訪問權限默認是私有的,因為列舉是常量,各個列舉常量,列舉值的個數在編譯期間就定義好了的,在運行時不能被改變
private Subject(String teacherName) {
if (teacherName != null && !teacherName.isEmpty())
this.teacherName = teacherName;
}
public String getTeacherName() {
return teacherName;
}
public void setTeacherName(String teacherName) {
this.teacherName = teacherName;
}
//實體代碼塊
{
teacherName = "未知";
}
}
注意點:
- 列舉的構造方法訪問權限是私有的,否則不會通過編譯
- 如果構造方法有引數,每一個列舉值需要提供引數的值,具體為什么,后面會講解
2.3. 列舉特性–定義靜態變數、靜態代碼塊以及靜態方法
這個就不做舉例了,大家可以自己驗證,
2.4. 列舉特性–列舉不能繼承其他類或列舉,也不可以被繼承,但是可以實作介面
具體為什么不能被繼承或繼承其他類,后面解釋
2.5. 列舉特性–可以用作switch陳述句
在JDK1.7以后,可以在switch中使用列舉,使用時,case陳述句中不能使用列舉型別名稱.列舉值的方式,編譯不會通過,例如:

直接寫列舉常量的名稱即可,
2.6. 列舉特性–enum不可以作為資料型別
enum實質上是java編譯器的語法糖,他不是一種資料型別,因此定義變數,函式形參定義,不可以用它做資料型別表示;

其實所有的列舉都是繼承一個java.lang.Enum抽象類的,所以列舉是型別別,不是一個新的型別,
/**
This is the common base class of all Java language enumeration types.
…
*/
public abstract class Enum<E extends Enum>implements Comparable, Serializable {
…
2.7. 列舉特性–enum可以宣告抽象方法
public enum Subject {
CHINESE("王老師"){
@Override
int score() {
return 80;
}
},
MATH("張老師"){
@Override
int score() {
return 100;
}
},
ENGLISH("Mr. Yang") {
@Override
int score() {
return 50;
}
};
private String teacherName;
private Subject(String teacherName) {
if (teacherName != null && !teacherName.isEmpty())
this.teacherName = teacherName;
}
{
teacherName = "未知";
}
abstract int score();
}
定義抽象方法時,每一個列舉常量實作該抽象方法
2.8. 列舉的常用方法
- name(),是一個實體方法,該方法在
java.lang.Enum中,回傳列舉的名稱,列舉的名稱就是定義列舉常量時用的字串,該方法被final修飾,因此不能被重寫 - values(),是一個靜態方法,按照宣告的順序回傳列舉類中定義的所有列舉常量組成的陣列

這個方法是一個隱含的方法,由編譯器生成的, - valueOf(String),是一個靜態方法,它根據一個名稱回傳一個列舉常量,

如果名稱所表示的列舉常量不存在,則拋出java.lang.IllegalArgumentException例外,這個方法是一個隱含的方法,由編譯器生成的,對于一個具體的列舉類來說,這個方法是有的,但是java.lang.Enum中沒有這個方法, - valueOf(Class,String),是一個靜態的方法,存在于
java.lang.Enum中,它的作用跟上一個方法類似,只不過第一個引數是Class型別的,需要指定獲取那個型別的常量,第二個引數是常量的名稱, - getDeclaringClass() ,是一個實體方法,存在于
java.lang.Enum中,可以獲取代表當前列舉型別的Class物件,被final關鍵字修飾,不能被重寫 - ordinal(),是一個實體方法,回傳當前列舉常量的序號,序號是在列舉類中宣告的順序,從0開始,最大值是
java.lang.Integer.MAX_VALUE,被final關鍵字修飾,不能被重寫 - toString(),是一個實體方法,來自于
java.lang.Object,在java.lang.Enum的實作是直接回傳了name屬性,就是name()方法的回傳值,這個方法可以被重寫, - compareTo(E) 是一個實體方法,
java.lang.Enum類實作了Comparable介面,用于比較當前列舉實體和指定的列舉實體,可以看一下它的實作

這是在JDK1.8中的實作版本,如果兩個列舉實體的型別都不一樣,直接會怕拋出例外,否則比較的是他們的ordinal值,這個值是ordinal()方法的回傳值,由于這個方法由final修飾,因此不能被重寫, - Enum(String , int ),這是
java.lang.Enum類的構造方法,在JDK1.8中,它的實作是這樣的:

這個方法程式員不能直接呼叫的,由編譯器生成代碼呼叫的,也就是說,編譯器會為我們自定義的列舉類生成構造方法,在生成的構造方法中,呼叫java.lang.Enum類的構造方法,這個我們會在下面進行驗證,
2.9. 列舉的常用工具類別庫
-
EnumSet
這是個抽象類,是列舉專用的Set介面的抽象實作,通過查看其add方法發現,它不允許null值,否則會拋出空指標例外,獲取它的物件一般有如下幾種靜態工廠方法:- EnumSet noneOf(Class elementType) ,根據elementType型別創建一個空的Set集合,如果傳的Class型別不是列舉,就會拋出類轉換例外
- EnumSet allOf(Class elementType),根據elementType型別創建一個包含elementType所有列舉常量的Set集合,如果傳的Class型別不是列舉,就會拋出類轉換例外
- EnumSet range(E from, E to),回傳一個包含ordinal值從from.ordinal()到to.ordinal()的所有列舉常量的Set集合(包括from和to),如果from.ordinal()大于to.ordinal(),則拋出
IllegalArgumentException例外 - EnumSet complementOf(EnumSet s),回傳的是所有E型別中不包含在s中的列舉常量的Set集合(補集),s不能為null
- EnumSet of(E e),回傳一個Set集合包含e,當然這個e不能為null,否則拋出空指標例外,這個方法有5個多載的版本,分別可以傳1-5個Enum引數
- EnumSet of(E first, E… rest) 這是上個方法的可變引數版本
- EnumSet copyOf(EnumSet s),創建一個包含s中所有元素的副本,呼叫clone方法實作的
- EnumSet copyOf(Collection c),創建一個包含c中所有列舉實體的Set,c不能為空集合,否則拋出
IllegalArgumentException例外
除了這些工廠方法之外,還有基本的add,addAll,remove,removeAll,contains,containsAll
,clear,equals等基本方法,就不一一介紹了,值得注意的是,EnumSet的實作類存盤Enum物件的方式是采用一個long型別的整數存盤的,使用位運算,基本操作在常數時間內完成,效率很高,有興趣的同學可以去看一下EnumSet的原始碼, -
EnumMap<K extends Enum, V>
這是一個Key型別是列舉的特定Map介面實作,不允許null Key,但允許null值,執行緒不安全的,獲取這個類的物件可以直接使用其構造方法:- public EnumMap(Class keyType) keyType顯然必須是列舉型別別,回傳一個空的Map集合
- public EnumMap(EnumMap<K, ? extends V> m) 傳入一個EnumMap實體,回傳一個副本
其他增刪改查的方法跟HashMap基本是一樣的,EnumMap把Key和Value都使用陣列的方式管理起來,根據Key的ordinal序號,作為Value陣列的下表來訪問Key對應的值,實作很簡潔,效率很高,
3. 列舉的實作原理
先從簡單的來,我現在把我們上面使用的Subject列舉類改成下面的樣子:
public enum Subject {
CHINESE,
MATH,
ENGLISH;
}
切換到class所在的目錄,然后:
javap -c Subject.class
javap是JDK自帶的反匯編工具,得到下面的結果:
Compiled from "Subject.java"
public final class com.victory.test.object_size.Subject extends java.lang.Enum<com.victory.test.object_size.Subject> {
public static final com.victory.test.object_size.Subject CHINESE;
public static final com.victory.test.object_size.Subject MATH;
public static final com.victory.test.object_size.Subject ENGLISH;
public static com.victory.test.object_size.Subject[] values();
Code:
0: getstatic #1 // Field $VALUES:[Lcom/victory/test/object_size/Subject;
3: invokevirtual #2 // Method "[Lcom/victory/test/object_size/Subject;".clone:()Ljava/lang/Object;
6: checkcast #3 // class "[Lcom/victory/test/object_size/Subject;"
9: areturn
public static com.victory.test.object_size.Subject valueOf(java.lang.String);
Code:
0: ldc #4 // class com/victory/test/object_size/Subject
2: aload_0
3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #4 // class com/victory/test/object_size/Subject
9: areturn
static {};
Code:
0: new #4 // class com/victory/test/object_size/Subject
3: dup
4: ldc #7 // String CHINESE
6: iconst_0
7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #9 // Field CHINESE:Lcom/victory/test/object_size/Subject;
13: new #4 // class com/victory/test/object_size/Subject
16: dup
17: ldc #10 // String MATH
19: iconst_1
20: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
23: putstatic #11 // Field MATH:Lcom/victory/test/object_size/Subject;
26: new #4 // class com/victory/test/object_size/Subject
29: dup
30: ldc #12 // String ENGLISH
32: iconst_2
33: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
36: putstatic #13 // Field ENGLISH:Lcom/victory/test/object_size/Subject;
39: iconst_3
40: anewarray #4 // class com/victory/test/object_size/Subject
43: dup
44: iconst_0
45: getstatic #9 // Field CHINESE:Lcom/victory/test/object_size/Subject;
48: aastore
49: dup
50: iconst_1
51: getstatic #11 // Field MATH:Lcom/victory/test/object_size/Subject;
54: aastore
55: dup
56: iconst_2
57: getstatic #13 // Field ENGLISH:Lcom/victory/test/object_size/Subject;
60: aastore
61: putstatic #1 // Field $VALUES:[Lcom/victory/test/object_size/Subject;
64: return
}
解釋:
- 當我們使用enum關鍵字定義列舉類時,java編譯器會自動幫我們生成一個類,繼承自
java.lang.Enum,(見第2行)并且生成的這個類是用final修飾的,因此列舉不能被繼承,也不能繼承其他類了,但是可以實作介面 - 我們宣告的每一個列舉常量,都是Subject類的一個實體(見第3,5,7行)
- 第9行和第16行是編譯器生成的values(),valueOf(String)靜態方法
- 第24行是編譯器插入的靜態代碼塊,后面是靜態代碼塊的JVM指令,舉例分析一下
- 第26行是使用new指令創建了一個Subject類的一個物件,
- 28行 ldc #7是把常量池中7號位置的常量壓入運算元堆疊,即把CHINESE這個字串的參考壓入運算元堆疊
- 第29行,iconst_0指令是把數字0壓入運算元堆疊
- 第30行invokespecial 是方法呼叫指令,可以看出,呼叫的是Subject類的<init>方法,<init>方法也就是構造方法,引數是(Ljava/lang/String;I)V,即一個String,一個整數,回傳void,這個方法是編譯器生成的,invokespecial 指令會把運算元堆疊的堆疊頂三個運算元依次彈出,然后執行init方法,通過分析我們知道依次彈出的是0,CHINESE,new出來的Subject類的物件的參考,其實這個0就是序號,即上面所說的ordinal()方法的回傳值,
- 對于Subject類的構造方法,通過jclasslib等位元組碼分析工具,可以看到其方法簽名如下:

其位元組碼指令如下:
java的每一個函式都有一個區域變數表,對于實體方法,區域變數表中第一個槽位放置的是當前物件的參考this,后面放置的依次是各個形參的參考,當呼叫方法的時候,形參參考的值被替換成實際的參考;
aload_0是把區域變數表的第一個槽位的值放到運算元堆疊,就是this,
aload_1是把區域變數表的第二個槽位的值放到運算元堆疊,就是String引數的參考,這個參考就是前面我們看到的列舉常量的名稱,
iload_2把區域變數表的第三個槽位的整數值放到運算元堆疊,就是序號,ordinal,
invokespecial就是呼叫java.lang.Enum的init方法,即父類的構造方法,在這里我們驗證了前面我們所說的編譯器生成自定義列舉類的構造方法,并呼叫了java.lang.Enum類的構造方法,對于JVM指令不太了解的同學可以移步查看Oracle官方檔案:JVM指令集參考
- 從第28行到第30行,是創建一個物件的程序,可以看出,在靜態代碼塊中這樣的程序執行了3次,也就是說,有幾個列舉常量,就會呼叫幾次列舉類的構造方法
- 如果自己定義了構造方法,例如在Subject類中做如下改動,
public enum Subject {
CHINESE("王老師"),
MATH("張老師"),
ENGLISH("Mr. Yang");
private String teacherName;
Subject(String teacherName){
this.teacherName=teacherName;
}
}
再次編譯,反匯編,看到的建構式如下:

可以看到,編譯器會把自己定義的引數,加在原來的構造方法后面,
- 對于含有抽象方法的情況,例如改成這樣:
public enum Subject {
CHINESE("王老師"){
@Override
int score() {
return 1;
}
},
MATH("張老師"){
@Override
int score() {
return 2;
}
},
ENGLISH("Mr. Yang"){
@Override
int score() {
return 3;
}
};
private String teacherName;
Subject(String teacherName){
this.teacherName=teacherName;
}
abstract int score();
}
有幾個列舉常量,會生成幾個Subject的內部類,

而Subject是一個繼承了java.lang.Enum的抽象類,每一個內部類都繼承自Subject,有興趣的讀者可以自己去驗證,
最后值得一提的是列舉有很好的安全性,由于列舉在編譯期間就確定了列舉常量的個數,因此JDK保證在運行時不能創建列舉物件,即使是使用反射,序列化和反序列化等手段也不行,因此列舉可以在某些要求安全性高的地方用作單例模式的實作方案,
到這里有關Java列舉的內容就總結完成了,你學會了嗎?如果有什么遺漏或錯誤,歡迎各位大神不吝賜教,
4. 參考文章
深入理解Java列舉型別(enum)
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/265111.html
標籤:java
上一篇:面試突擊版(JVM+Redis+SSM+SpringBoot+MQ)
下一篇:線性結構:稀疏陣列
