詳解 Java 枚舉

本文大綱:

前言

枚舉(enum)是 Java 語言的關鍵字之一,和 class 關鍵字一樣,我們可以通過 enum 來定義一個枚舉類,並在這個枚舉類裏面創建相關的枚舉常量。這篇文章裏我們來看一下枚舉在 Java 字節碼層面是怎麼表示的。

使用枚舉

我們通過 enum 關鍵字來定義枚舉,一個簡單的枚舉定義如下:

public enum TestEnum {
	X("x"), Y("y"), Z("z");

	private String value;

	TestEnum(String value) {
		this.value = value;
	}

    public String getValue() {
		return value;
	}
}

需要注意的是,即使我們在 TestEnum 中定義的構造方法沒有添加任何的訪問修飾符,它也不能在任何類中被調用(包括 TestEnum 本身)。 因爲枚舉本來就是需要在定義時就創建好對應的實例。如果你嘗試調用,會出現語法錯誤。我們在後面還會討論到這個問題。

在使用枚舉常量的時候,我們會發現一個有趣的現象:
在這裏插入圖片描述
我們在定義的枚舉 TestEnum 中並沒有爲其添加 compareTonameordinal 等方法,那這些多出來的方法哪來的呢?

枚舉類

我們來深入看一下 Java 編譯器是怎麼處理枚舉的:在控制檯上鍵入 javap -c TestEnum.class(類路徑需要換成你自己的編譯得到的類路徑),即反編譯查看 TestEnum.class 的字節碼:

/* 生成的是一個自定義的類,繼承於 java.lang.Enum<E extends Enum<E>> */
public final class enum_.TestEnum extends java.lang.Enum<enum_.TestEnum> {
  /* 定義了三個常量,即爲我們在定義時書寫的三個枚舉常量 */
  public static final enum_.TestEnum X;

  public static final enum_.TestEnum Y;

  public static final enum_.TestEnum Z;

  /* 編譯器生成的靜態代碼塊,用來創建三個枚舉常量(X, Y, Z)並賦值 */
  static {};
    Code:
       0: new           #1                  // class enum_/TestEnum
       3: dup
       4: ldc           #16                 // String X
       6: iconst_0
       7: ldc           #17                 // String x
       9: invokespecial #19                 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
      12: putstatic     #23                 // Field X:Lenum_/TestEnum;
          // 以上是常量 X 的創建及初始化
      15: new           #1                  // class enum_/TestEnum
      18: dup
      19: ldc           #25                 // String Y
      21: iconst_1
      22: ldc           #26                 // String y
      24: invokespecial #19                 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
      27: putstatic     #28                 // Field Y:Lenum_/TestEnum;
          // 以上是常量 Y 的創建及初始化
      30: new           #1                  // class enum_/TestEnum
      33: dup
      34: ldc           #30                 // String Z
      36: iconst_2
      37: ldc           #31                 // String z
      39: invokespecial #19                 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
      42: putstatic     #33                 // Field Z:Lenum_/TestEnum;
          // 以上是常量 Z 的創建及初始化
      45: iconst_3
      46: anewarray     #1                  // class enum_/TestEnum
      49: dup
      50: iconst_0
      51: getstatic     #23                 // Field X:Lenum_/TestEnum;
      54: aastore
      55: dup
      56: iconst_1
      57: getstatic     #28                 // Field Y:Lenum_/TestEnum;
      60: aastore
      61: dup
      62: iconst_2
      63: getstatic     #33                 // Field Z:Lenum_/TestEnum;
      66: aastore
      67: putstatic     #35                 // Field ENUM$VALUES:[Lenum_/TestEnum;
      70: return

  public static enum_.TestEnum[] values();
    Code:
       0: getstatic     #35                 // Field ENUM$VALUES:[Lenum_/TestEnum;
       3: dup
       4: astore_0
       5: iconst_0
       6: aload_0
       7: arraylength
       8: dup
       9: istore_1
      10: anewarray     #1                  // class enum_/TestEnum
      13: dup
      14: astore_2
      15: iconst_0
      16: iload_1
      17: invokestatic  #47                 // Method java/lang/System.arraycopy:(Ljava/lang/Object;ILjava/lang/Object;II)V
      20: aload_2
      21: areturn

  public static enum_.TestEnum valueOf(java.lang.String);
    Code:
       0: ldc           #1                  // class enum_/TestEnum
       2: aload_0
       3: invokestatic  #55                 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
       6: checkcast     #1                  // class enum_/TestEnum
       9: areturn
}

從字節碼中,我們知道:創建的枚舉類經過編譯器處理後會生成一個類,這個類繼承於 java.lang.Enum 類,我們可以看看這個類的相關代碼:

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
    // ... 
    private final String name;

    public final String name() {
        return name;
    }
    
    private final int ordinal;

    public final int ordinal() {
        return ordinal;
    }

    /**
     * Sole constructor.  Programmers cannot invoke this constructor.
     * It is for use by code emitted by the compiler in response to
     * enum type declarations.
     *
     * @param name - The name of this enum constant, which is the identifier
     *               used to declare it.
     * @param ordinal - The ordinal of this enumeration constant (its position
     *         in the enum declaration, where the initial constant is assigned
     *         an ordinal of zero).
     */
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
    // ...

可以看到,Enum 類本身提供了兩個屬性(name 和 ordinal)來標識某個枚舉對象,name 即爲該枚舉對象的名字,而 ordinal 爲該枚舉對象所在所有枚舉常量中的序號(從 0 開始,比如上述的枚舉常量 X, Y, Z 中,X.ordinal 爲 0,Y.ordinal 爲 1,Z.ordinal 爲 2)。同時這個類實現了 Comparable 接口,提供了兩個方法 name()oridinal() ,這樣的話我們就知道文章開頭中說到的那幾個方法是哪裏來的了:就是父類提供的。

消失的構造方法

我們再回頭看一下 TestEnum 類的字節碼,你會發現即使 TestEnum 已經被作爲一個類來處理了,但是在其字節碼中並沒有看到任何構造方法,但是我們在代碼中定義 TestEnum 類時明明提供了一個帶有一個參數的構造方法。要知道,即使一個最簡單的類的都會默認有一個無參構造方法,而在對應的字節碼中時可以看到的。我們來試試,寫一個空類 EnumTest.java:

public class EmptyClass {
}

反編譯得到的字節碼如下:

public class enum_.EmptyClass {
  public enum_.EmptyClass();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return
}

可以看到,編譯器給 EmptyClass 類提供了一個空的構造方法。那麼爲什麼在上面的枚舉類 TestEnum 的字節碼中我們沒有看到構造方法呢?**我們可以猜到這正是編譯器爲了顧及枚舉的特性而將構造方法去除(或者說用別的方法代替)的體現。**我們回到 TestEnum 的字節碼中,在 static{} 塊的第 9 、24、39 行字節碼中都調用了invokespecial 指令,即調用某個方法,而調用的具體方法爲:"<init>":(Ljava/lang/String;ILjava/lang/String;)V,我們知道,編譯器在編譯某個類時會爲該類提供一個 方法,這個方法會在創建該類對象的時候調用。如果你熟悉字節碼中方法簽名規則的話,很容易就可以復原出這個方法的參數列表和返回值:void <init>(String , int , String ); 。如果你還不不知道字節碼中方法的簽名規則,可以參考附錄。

有了這個方法,我們大概就知道參數的含義了:第一和第二個參數爲 Enum 類(TestEnum 的父類)傳入的值(即 nameordinal),而第三個參數就是在 TestEnum 類中定義的構造方法中的 value 參數。我們還可以進一步測試,將 TestEnum 的構造方法改爲接收兩個參數:

private TestEnum(String value, int valueInt) {
    this.value = value;
    this.valueInt = valueInt;
}

此時反編譯得到的 <init> 方法的簽名如下:
"<init>":(Ljava/lang/String;ILjava/lang/String;I)V
還原出來的方法參數和返回值如下:
void <init>(String , int , String , int );
有興趣的小夥伴可以試試其他參數。
相比之前的 <init> 方法,該方法末尾多了一個 int 類型的參數,即爲我們新加入的參數。
至此我們可以得出結論了:對於枚舉類來說,編譯時編譯器會將其構造方法去除,將其功能和參數都放在了 <init> 方法中,也就是說在枚舉類的字節碼中 <init> 方法充當了構造方法的作用。同時,編譯器會爲其添加靜態代碼塊,在靜態代碼塊中完成枚舉類中聲明的枚舉常量的創建。
因此我們不能在任何地方調用枚舉類的構造方法,因爲在字節碼層面其已經被去除了。枚舉對象只能在枚舉類定義的時候進行實例化,即只能創建有限個枚舉對象。這也符合枚舉的詞義(枚舉即有限)。

枚舉和 switch

我們都用過 switch 語句,對於普通常量(int 等)使用 switch 語句時其直接將 switch 中引用的值和 case 中引用的常量值一一比較。如果 switch 引用的是 String 對象,則通過該對象的 hashCode 方法的返回值和 case 中 String 類型常量的 hashCode 方法返回值進行比較,同時在比較完成後還會使用 equals 方法再次驗證。

那麼對於枚舉是怎麼樣的情況呢,其實到這裏很多小夥伴已經可以猜到了,我們寫個 demo 驗證一下:

public class Enum {


    public static void main(String[] args) {
        TestEnum testEnum = TestEnum.X;
        switch (testEnum) {
            case X: {
                break;
            }
            case Y: {
                break;
            }
            case Z: {
                break;
            }
        }
    }
}

看看此時 Enum 類的 main 方法反編譯後的字節碼:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: getstatic     #2                  // Field TestEnum.X:LTestEnum;
         3: astore_1
         4: getstatic     #3                  // Field Enum$1.$SwitchMap$TestEnum:[I
         7: aload_1
         8: invokevirtual #4                  // Method TestEnum.ordinal:()I
        11: iaload // 這裏加載了一個 int 類型的數組,用來給 tableswitch 字節碼做下標->跳轉行的索引
        12: tableswitch   { // 1 to 3
                       1: 40
                       2: 43
                       3: 46
                 default: 46
            }
        40: goto          46
        43: goto          46
        46: return

從第 8 行字節碼中可以知道,對於枚舉,switch 語句也是調用其 oridinal 方法轉換爲整數之後再執行 tableswitch 字節碼,通過數組下標直接索引。

好了,這篇文章就到這裏了,相信到了這裏你對 Java 枚舉已經有了一個的詳細的理解。如果覺得文章有什麼不正確的地方,請多多指點,如果覺得本篇文章對你有幫助,請不要吝嗇你的贊。

謝謝觀看。。。




附:字節碼中的方法簽名

字節碼中以 方法所屬類全限定名.方法名:(參數類型及列表)返回值 來描述一個方法,方法參數和返回值字符含義如下表:

字符 數據類型 特殊說明
V void 用於表示方法的返回值
Z boolean
B byte
C char
S short
I int
J long
F float
D double

[ 代表 數組,以 [ 開頭,配合其他的特殊字符,表示對應數據類型的數組,幾個 [ 表示幾維數組,比如一維的 int 數組對應的就是:[I
L 代表引用類型,以 L 開頭,; 結尾,中間是引用類的全限定名
比如 java.lang.String 對應的就是:Ljava/lang/String;

我們來看一個例子:將簽名爲 arraycopy(Ljava/lang/Object;ILjava/lang/Object;II)V 的方法進行還原:

根據上表,這個方法的第一個參數類型爲 Object,第二個參數類型爲 int,第三個參數類型爲 Object,第四個參數類型爲 int,第五個參數類型爲 int。返回值類型爲 void。由此得出方法原型爲:void arraycopy(Object , int , Object , int , int )

有兩點需要注意:

1、這裏的方法簽名中包括了返回值,但是這並不能作爲方法重載的依據,方法重載時判斷兩個方法是否是同一個方法中只有方法名和方法參數列表,沒有返回值類型。(可以理解成一個是語法層面的,一個是反編譯後字節碼對方法的解釋層面的)。

2、表示引用類型的時候末尾需要加上 ; 表示結束,此時 L 之後直到 ; 之前的內容就代表引用類型的類全限定名。例:Ljava/lang/String;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章