java列舉型別的實現原理
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。
如何定義列舉型別
比如表示加減乘除操作,我們可以定義如下列舉:
package com.mikan;
/**
* @author Mikan
* @date 2015-08-29 12:06
*/
public enum Operator {
ADD,
SUBTRACT,
MULTIPLY,
DIVIDE
}
上面的列舉定義了四個列舉常量,同時,在列舉中還可以定義普通方法、抽象方法,如下所示:
從上面可以看到,我們基本可以像定義類一樣來定義列舉。我們還可以定義屬性、構造方法等:package com.mikan; /** * @author Mikan * @date 2015-08-29 12:06 */ public enum Operator { ADD { @Override public int calculate(int a, int b) { return a + b; } }, SUBTRACT { @Override public int calculate(int a, int b) { return a - b; } }, MULTIPLY { @Override public int calculate(int a, int b) { return a * b; } }, DIVIDE { @Override public int calculate(int a, int b) { if (b == 0) { throw new IllegalArgumentException("divisor must not be 0"); } return a / b; } }; public abstract int calculate(int a, int b); }
package com.mikan; /** * @author Mikan * @date 2015-08-29 12:06 */ public enum Operator { ADD ("+") { @Override public int calculate(int a, int b) { return a + b; } }, SUBTRACT ("-") { @Override public int calculate(int a, int b) { return a - b; } }, MULTIPLY ("*") { @Override public int calculate(int a, int b) { return a * b; } }, DIVIDE ("/") { @Override public int calculate(int a, int b) { if (b == 0) { throw new IllegalArgumentException("divisor must not be 0"); } return a / b; } }; Operator (String operator) { this.operator = operator; } private String operator; public abstract int calculate(int a, int b); public String getOperator() { return operator; } }
實現原理分析
既然可以像使用普通的類一樣使用列舉,編譯器究竟為我們做了些什麼事呢?要想知道這其中的祕密,最有效的途徑就是檢視生成的位元組碼。下面就來看看上面定義的列舉生成的位元組碼是怎麼樣的。首先來看看反編譯的基本資訊:
localhost:mikan mikan$ javap Operator.class
Compiled from "Operator.java"
public abstract class com.mikan.Operator extends java.lang.Enum<com.mikan.Operator> {
public static final com.mikan.Operator ADD;
public static final com.mikan.Operator SUBTRACT;
public static final com.mikan.Operator MULTIPLY;
public static final com.mikan.Operator DIVIDE;
public static com.mikan.Operator[] values();
public static com.mikan.Operator valueOf(java.lang.String);
public abstract int calculate(int, int);
public java.lang.String getOperator();
com.mikan.Operator(java.lang.String, int, java.lang.String, com.mikan.Operator$1);
static {};
}
可以看到,一個列舉在經過編譯器編譯過後,變成了一個抽象類,它繼承了java.lang.Enum;而列舉中定義的列舉常量,變成了相應的public static final屬性,而且其型別就抽象類的型別,名字就是列舉常量的名字,同時我們可以在Operator.class的相同路徑下看到四個內部類的.class檔案com/mikan/Operator$1.class、com/mikan/Operator$2.class、com/mikan/Operator$3.class、com/mikan/Operator$4.class,也就是說這四個命名欄位分別使用了內部類來實現的;同時添加了兩個方法values()和valueOf(String);我們定義的構造方法本來只有一個引數,但卻變成了三個引數;同時還生成了一個靜態程式碼塊。這些具體的內容接下來仔細看看。
看下面詳細的反編譯資訊:
localhost:mikan mikan$ javap -c -v Operator.class
Classfile /Users/mikan/Documents/workspace/project/algorithm/target/classes/com/mikan/Operator.class
Last modified 2015-8-29; size 1720 bytes
MD5 checksum 478439554cb827fec3c36cf51c8d36da
Compiled from "Operator.java"
public abstract class com.mikan.Operator extends java.lang.Enum<com.mikan.Operator>
Signature: #67 // Ljava/lang/Enum<Lcom/mikan/Operator;>;
SourceFile: "Operator.java"
InnerClasses:
static #24; //class com/mikan/Operator$4
static #19; //class com/mikan/Operator$3
static #14; //class com/mikan/Operator$2
static #9; //class com/mikan/Operator$1
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER, ACC_ABSTRACT, ACC_ENUM
Constant pool:
// 省略常量池資訊
{
public static final com.mikan.Operator ADD;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final com.mikan.Operator SUBTRACT;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final com.mikan.Operator MULTIPLY;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static final com.mikan.Operator DIVIDE;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static com.mikan.Operator[] values();
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #2 // Field $VALUES:[Lcom/mikan/Operator;
3: invokevirtual #3 // Method "[Lcom/mikan/Operator;".clone:()Ljava/lang/Object;
6: checkcast #4 // class "[Lcom/mikan/Operator;"
9: areturn
LineNumberTable:
line 7: 0
public static com.mikan.Operator valueOf(java.lang.String);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc_w #5 // class com/mikan/Operator
3: aload_0
4: invokestatic #6 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
7: checkcast #5 // class com/mikan/Operator
10: areturn
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 name Ljava/lang/String;
public abstract int calculate(int, int);
flags: ACC_PUBLIC, ACC_ABSTRACT
public java.lang.String getOperator();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #8 // Field operator:Ljava/lang/String;
4: areturn
LineNumberTable:
line 46: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/mikan/Operator;
com.mikan.Operator(java.lang.String, int, java.lang.String, com.mikan.Operator$1);
flags: ACC_SYNTHETIC
Code:
stack=4, locals=5, args_size=5
0: aload_0
1: aload_1
2: iload_2
3: aload_3
4: invokespecial #1 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
7: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this Lcom/mikan/Operator;
0 8 1 x0 Ljava/lang/String;
0 8 2 x1 I
0 8 3 x2 Ljava/lang/String;
0 8 4 x3 Lcom/mikan/Operator$1;
static {};
flags: ACC_STATIC
Code:
stack=5, locals=0, args_size=0
0: new #9 // class com/mikan/Operator$1
3: dup
4: ldc #10 // String ADD
6: iconst_0
7: ldc #11 // String +
9: invokespecial #12 // Method com/mikan/Operator$1."<init>":(Ljava/lang/String;ILjava/lang/String;)V
12: putstatic #13 // Field ADD:Lcom/mikan/Operator;
15: new #14 // class com/mikan/Operator$2
18: dup
19: ldc #15 // String SUBTRACT
21: iconst_1
22: ldc #16 // String -
24: invokespecial #17 // Method com/mikan/Operator$2."<init>":(Ljava/lang/String;ILjava/lang/String;)V
27: putstatic #18 // Field SUBTRACT:Lcom/mikan/Operator;
30: new #19 // class com/mikan/Operator$3
33: dup
34: ldc #20 // String MULTIPLY
36: iconst_2
37: ldc #21 // String *
39: invokespecial #22 // Method com/mikan/Operator$3."<init>":(Ljava/lang/String;ILjava/lang/String;)V
42: putstatic #23 // Field MULTIPLY:Lcom/mikan/Operator;
45: new #24 // class com/mikan/Operator$4
48: dup
49: ldc #25 // String DIVIDE
51: iconst_3
52: ldc #26 // String /
54: invokespecial #27 // Method com/mikan/Operator$4."<init>":(Ljava/lang/String;ILjava/lang/String;)V
57: putstatic #28 // Field DIVIDE:Lcom/mikan/Operator;
60: iconst_4
61: anewarray #5 // class com/mikan/Operator
64: dup
65: iconst_0
66: getstatic #13 // Field ADD:Lcom/mikan/Operator;
69: aastore
70: dup
71: iconst_1
72: getstatic #18 // Field SUBTRACT:Lcom/mikan/Operator;
75: aastore
76: dup
77: iconst_2
78: getstatic #23 // Field MULTIPLY:Lcom/mikan/Operator;
81: aastore
82: dup
83: iconst_3
84: getstatic #28 // Field DIVIDE:Lcom/mikan/Operator;
87: aastore
88: putstatic #2 // Field $VALUES:[Lcom/mikan/Operator;
91: return
LineNumberTable:
line 9: 0
line 15: 15
line 21: 30
line 27: 45
line 7: 60
}
localhost:mikan mikan$
下面分析一下位元組碼中的各部分,其中:
InnerClasses:
static #24; //class com/mikan/Operator$4
static #19; //class com/mikan/Operator$3
static #14; //class com/mikan/Operator$2
static #9; //class com/mikan/Operator$1
從中可以看到它有4個內部類,這四個內部類的詳細資訊後面會分析。
靜態程式碼塊:
static {};
flags: ACC_STATIC
Code:
stack=5, locals=0, args_size=0
// 建立一個Operator$1的內部類物件
0: new #9 // class com/mikan/Operator$1
3: dup
// 接下來的三條指令分別是把三個引數推送到棧頂,然後呼叫Operator$1的編譯器生成的<init>方法
4: ldc #10 // String ADD
6: iconst_0
7: ldc #11 // String +
// 呼叫<init>方法
9: invokespecial #12 // Method com/mikan/Operator$1."<init>":(Ljava/lang/String;ILjava/lang/String;)V
// 設定ADD屬性的值為新建立的物件
12: putstatic #13 // Field ADD:Lcom/mikan/Operator;
// 接下來是分別初始化另外三個屬性SUBTRACT、MULTIPLY、DIVIDE,這裡就不再重複
15: new #14 // class com/mikan/Operator$2
18: dup
19: ldc #15 // String SUBTRACT
21: iconst_1
22: ldc #16 // String -
24: invokespecial #17 // Method com/mikan/Operator$2."<init>":(Ljava/lang/String;ILjava/lang/String;)V
27: putstatic #18 // Field SUBTRACT:Lcom/mikan/Operator;
30: new #19 // class com/mikan/Operator$3
33: dup
34: ldc #20 // String MULTIPLY
36: iconst_2
37: ldc #21 // String *
39: invokespecial #22 // Method com/mikan/Operator$3."<init>":(Ljava/lang/String;ILjava/lang/String;)V
42: putstatic #23 // Field MULTIPLY:Lcom/mikan/Operator;
45: new #24 // class com/mikan/Operator$4
48: dup
49: ldc #25 // String DIVIDE
51: iconst_3
52: ldc #26 // String /
54: invokespecial #27 // Method com/mikan/Operator$4."<init>":(Ljava/lang/String;ILjava/lang/String;)V
57: putstatic #28 // Field DIVIDE:Lcom/mikan/Operator;
// 下面是new了一個長度為4的Operator型別的陣列,並分別設定陣列中各元素的值為上面的四個屬性的值
60: iconst_4
61: anewarray #5 // class com/mikan/Operator
64: dup
65: iconst_0
66: getstatic #13 // Field ADD:Lcom/mikan/Operator;
69: aastore
70: dup
71: iconst_1
72: getstatic #18 // Field SUBTRACT:Lcom/mikan/Operator;
75: aastore
76: dup
77: iconst_2
78: getstatic #23 // Field MULTIPLY:Lcom/mikan/Operator;
81: aastore
82: dup
83: iconst_3
84: getstatic #28 // Field DIVIDE:Lcom/mikan/Operator;
87: aastore
//下面是設定屬性$VALUES的值為剛建立的陣列
88: putstatic #2 // Field $VALUES:[Lcom/mikan/Operator;
91: return
其實編譯器生成的這個靜態程式碼塊做了如下工作:分別設定生成的四個公共靜態常量欄位的值,同時編譯器還生成了一個靜態欄位$VALUES,儲存的是列舉型別定義的所有列舉常量。相當於下面的程式碼:
Operator ADD = new Operator1();
Operator SUBTRACT = new Operator1();
Operator MULTIPLY = new Operator1();
Operator DIVIDE = new Operator1();
Operator[] $VALUES = new Operator[4];
$VALUES[0] = ADD;
$VALUES[1] = SUBTRACT;
$VALUES[2] = MULTIPLY;
$VALUES[3] = DIVIDE;
編譯器新增的values方法:
public static com.mikan.Operator[] values();
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #2 // Field $VALUES:[Lcom/mikan/Operator;
3: invokevirtual #3 // Method "[Lcom/mikan/Operator;".clone:()Ljava/lang/Object;
6: checkcast #4 // class "[Lcom/mikan/Operator;"
9: areturn
這個方法是一個公共的靜態方法,所以我們可以直接呼叫該方法(Operator.values()),返回這個列舉值的陣列,另外,這個方法的實現是,克隆在靜態程式碼塊中初始化的$VALUES欄位的值,並把型別強轉成Operator[]型別返回。它相當於下面的程式碼:
public static com.mikan.Operator[] values() {
return (Operator[])$VALUES.clone();
}
編譯器新增的valueOf方法:
public static com.mikan.Operator valueOf(java.lang.String);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc_w #5 // class com/mikan/Operator
3: aload_0
4: invokestatic #6 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
7: checkcast #5 // class com/mikan/Operator
10: areturn
這個方法是一個公共的靜態方法,所以我們可以直接呼叫該方法(Operator.valueOf()),返回引數字串表示的列舉常量,另外,這個方法的實現是,呼叫父類Enum的valueOf方法,並把型別強轉成Operator。它相當於如下的程式碼:
public static com.mikan.Operator valueOf(String name) {
return (Operator)Enum.valueOf(Operator.class, name);
}
生成的內部類
下面看看生成的內部類Operator$1:
localhost:mikan mikan$ javap Operator\$1.class
Compiled from "Operator.java"
final class com.mikan.Operator$1 extends com.mikan.Operator {
com.mikan.Operator$1(java.lang.String, int, java.lang.String);
public int calculate(int, int);
}
localhost:mikan mikan$
可以看到,實現內部類是繼承自Operator,即
ADD {
@Override
public int calculate(int a, int b) {
return a + b;
}
},
這就是說,我們定義的每個列舉常量,最終都生成了一個像上面這樣的內部類。
構造方法為什麼增加了兩個引數?
有一個問題,構造方法我們明明只定義了一個引數,為什麼生成的構造方法是三個引數呢?
從Enum類中我們可以看到,為每個列舉都定義了兩個屬性,name和ordinal,name表示我們定義的列舉常量的名稱,如ADD、SUBTRACT等,而ordinal是一個順序號,根據定義的順序分別賦予一個整形值,從0開始。在列舉常量初始化時,會自動為初始化這兩個欄位,設定相應的值,所以才在構造方法中添加了兩個引數。即:
com.mikan.Operator$1(String name, int ordinal, String operator);
另外三個列舉常量生成的內部類基本上差不多,這裡就不重複說明了。
我們可以從Enum類的程式碼中看到,定義的name和ordinal屬性都是final的,而且大部分方法也都是final的,特別是clone、readObject、writeObject這三個方法,這三個方法和列舉通過靜態程式碼塊來進行初始化一起,它保證了列舉型別的不可變性,不能通過克隆,不能通過序列化和反序列化來複制列舉,這能保證一個列舉常量只是一個例項,即是單例的,所以在effective java中推薦使用列舉來實現單例。
總結
列舉本質上是通過普通的類來實現的,只是編譯器為我們進行了處理。每個列舉型別都繼承自java.lang.Enum,並自動添加了values和valueOf方法。而每個列舉常量是一個靜態常量欄位,使用內部類實現,該內部類繼承了列舉類。所有列舉常量都通過靜態程式碼塊來進行初始化,即在類載入期間就初始化。另外通過把clone、readObject、writeObject這三個方法定義為final的,同時實現是丟擲相應的異常。這樣保證了每個列舉型別及列舉常量都是不可變的。可以利用列舉的這兩個特性來實現執行緒安全的單例。