位元組碼層面深入分析Java枚舉類

枚舉類的使用

定義一個簡單的枚舉類,其中包含若干枚舉常量,示例如下:

public enum Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY,THURSDAY, FRIDAY, SATURDAY 
}

Java 的 switch 語句參數支援使用枚舉類

// day是Day類型變數
switch (day) {
    case MONDAY:
        System.out.println("要開組會了好難受");
        break;
    case THURSDAY:
        System.out.println("肯德基瘋狂星期四");
        break;
    case SATURDAY: case SUNDAY:
        System.out.println("不浪對不起周末");
        break;
    default:
        System.out.println("摸摸魚");
        break;
}

其實這個用 static final 也可以,但是用枚舉類的好處在於:

  • 用 static final 的話,傳入的變數就需要進行參數檢查,而枚舉類不用,因為肯定在枚舉的範圍中,或為 null
  • static final 不支援屬性擴展,每個變數名對應一個值,而每一個枚舉值可以擁有自己的多個屬性(欄位)

枚舉類可以添加屬性及相應的構造器,以及方法,不過枚舉類中的構造器默認也必須是 private 的。示例如下:

public enum Day {
    MONDAY(1, "星期一"),
    TUESDAY(2, "星期二"),
    // ......
    SUNDAY(7, "星期日");

    private int index;
    private String name;
    
    Day(int index, String name) {
        this.index = index;
        this.name = name;
    }
    
    // 針對index、name的getter方法
    // ......
}

一般來說,不會為枚舉類添加 setter 方法,因為這樣會枚舉一般只用來做常量,setter 會破壞它的常量特性

枚舉類中的每個枚舉常量都可以實現枚舉主類定義的(abstract)方法,也可以擁有各自的內部方法,如下:

public enum Day {
    MONDAY(1, "星期一"){
        @Override
        public Day getNext() {
            return TUESDAY;
        }
    },
    TUESDAY(2, "星期二"){
        @Override
        public Day getNext() {
            return WEDNESDAY;
        }
    },
    // ......
    SUNDAY(7, "星期日"){
        @Override
        public Day getNext() {
            return MONDAY;
        }
    };

    private int index;
    private String name;
    
    Day(int index, String name) {
        this.index = index;
        this.name = name;
    }
    
    // 在主類中定義抽象方法
    public abstract Day getNext();

    // 針對index、name的getter方法
    // ......
}

雖然也可以在每個枚舉常量中自定義任何方法,但是如果沒有在主類中聲明,就不能訪問到,這個暫且按下不表,原因在後面會解釋

所有枚舉類都默認繼承了 Enum 類,可以直接使用 Enum 提供的實用方法。由於 Java 只支援單繼承,所以枚舉類不能再繼承別的父類,只能實現介面。一些使用的 Enum 類的方法,都貼在了文末_

枚舉類的枚舉常量全局唯一,不存在並發安全性問題,且不會被反射、序列化方式惡意創建新的枚舉常量對象,很適合用來實現單例模式。這裡可以參加部落客的另一篇文章:單例模式的各種實現方式(Java)

最後再補充一點,部落客發現某書和很多部落格都說:在比較兩個枚舉類型的值時 , 永遠不需要調用 equals 方法, 而直接使用 == 就可以了。但是我看了下 Enum 類中給到的 equals 源碼(貼在下面),實際上用的也是 ==,我自己手動測試了也沒問題。但不知道為什麼,大家的部落格上都這麼寫的,難道真就是人云亦云嗎-_-||

public final boolean equals(Object other) {
    return this==other;
}
作者:酒冽        出處://www.cnblogs.com/frankiedyz/p/15851123.html
版權:本文版權歸作者和部落格園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接;否則必究法律責任

枚舉類底層原理探究

研究一個問題,最好是從現象出發去看本質,先知道有哪些現象,再看看它們的本質原因是什麼。對於枚舉類來說,它和普通類的不同之處就是現象:

  • 枚舉類不能由外界創建對象
  • 枚舉類默認繼承了 Enum 類,不可以再繼承其他類,且枚舉類不可以被繼承
  • 雖然 Enum 類中並沒有 values() 和 valueOf(String) 方法,但是枚舉類也可以調用
  • 枚舉主類可以定義 abstract 方法,每個枚舉常量都可以分別對其提供實現,不過它們也可以自定義其他方法。但只有在枚舉主類定義過的方法,才能通過枚舉常量調用到,否則不能
  • 定義在每個枚舉主類中的欄位,在各個枚舉常量中卻是不同的
  • 枚舉類的所有枚舉常量都是一開始就創建好的,全局唯一、不可變且執行緒安全

先編寫一段普通的枚舉類作為示例,程式碼如下:

public enum Color {
    red("紅", 0) {
        @Override
        public void print() {
            System.out.println(getName() + ":" + getIndex());
        }
    },
    green("綠", 1) {
        @Override
        public void print() {
            System.out.println(getName() + " " + getIndex());
        }
    };

    private String name;
    private int index;

    Color() {
    }

    Color(String name, int index) {
        this.name = name;
        this.index = index;
    }

    // 枚舉主類中定義的抽象方法
    public abstract void print();

    // name、index的getterr方法
}

先對 Color.class 進行反編譯(javap 不加參數 -v):

Compiled from "Color.java"
public abstract class com.duyuzhou.enumTest.Color extends java.lang.Enum<com.duyuzhou.enumTest.Color> {
  public static final com.duyuzhou.enumTest.Color red;
  public static final com.duyuzhou.enumTest.Color green;
  public static com.duyuzhou.enumTest.Color[] values();
  public static com.duyuzhou.enumTest.Color valueOf(java.lang.String);
  public java.lang.String getName();
  public int getIndex();
  public abstract void print();
  public static void main(java.lang.String[]);
  com.duyuzhou.enumTest.Color(java.lang.String, int, java.lang.String, int, com.duyuzhou.enumTest.Color$1);
  static {};
}

仔細分析,就可以得出以下結論:

  • 枚舉類繼承了 Enum 類,以及 Enum 類的各種方法。編譯器為枚舉類添加了 final 關鍵字,使得枚舉類不能被其他類繼承
  • 枚舉類被編譯器額外添加了兩個 static 方法:values() 和 valueOf(String)
  • 枚舉類會被編譯成 abstract 類,因此枚舉類可以定義抽象方法
  • 枚舉類的構造器都被改為了 private,因此外界不可以使用枚舉類創建對象
  • 所有枚舉常量都會被編譯成枚舉主類的 static final 屬性,那麼在類載入的初始化階段就會將所有枚舉常量創建好,而且只會創建一次。final 也能保證枚舉常量一旦被創建好後,對於所有執行緒都是可見的,不會存在執行緒安全問題

如果反編譯加上 -v 參數,可以看到 Color 有兩個靜態內部類,分別是 Color$1 和 Color$2,對其中一個進行反編譯(不加 -v):

Compiled from "Color.java"
final class com.duyuzhou.enumTest.Color$1 extends com.duyuzhou.enumTest.Color {
  com.duyuzhou.enumTest.Color$1(java.lang.String, int, java.lang.String, int);
  public void print();
}

其實每個靜態內部類都對應了一個枚舉常量,這些靜態內部類都繼承了枚舉主類,所以枚舉常量中可以實現主類中定義的 abstract 方法。而且,在枚舉主類中,每個枚舉常量都變成了一個枚舉主類類型的欄位,因此外界不可能調用一個枚舉常量中私自定義但枚舉主類中沒定義的方法

此外,由於每個枚舉常量都是不同枚舉子類的一個對象,所以它們各自繼承了父類定義的欄位,且觀察枚舉常量的反編譯結果會發現,編譯器為每個枚舉子類都添加了一個構造函數,所以枚舉主類中定義的欄位是在各個枚舉常量中分開賦值的

至此,上面的所有「現象」,都通過反編譯查看位元組碼的方式得到了解答,本質上是編譯器幫我們做好了幕後工作,所以才有了這些程式碼中看不到卻實際存在的規則。不過,還遺留了一個小問題——在 Color.class 中,為什麼編譯器會為 Color 的構造器額外添加兩個方法參數:String 和 int 型呢?

來看看 Color 的構造器 com.duyuzhou.enumTest.Color(java.lang.String, int, java.lang.String, int, com.duyuzhou.enumTest.Color$1) 反編譯出的位元組碼:

0 aload_0
1 aload_1
2 iload_2
3 aload_3
4 invokespecial #2 <enumtest/Color.<init> : (Ljava/lang/String;ILjava/lang/String;)V>
7 return

這裡會調用 Color.<init> 方法,該方法的位元組碼需要藉助 Jclasslib 工具查看,如下:

0 aload_0
1 aload_1
2 iload_2
3 invokespecial #9 <java/lang/Enum.<init> : (Ljava/lang/String;I)V>
6 return

看到這裡應該就能懂了,這裡實際上會將額外添加的兩個方法參數傳遞給父類 Enum 的構造器,那麼看一下 Enum 中接收一個 String 和一個 int 型的構造器是怎樣的:

protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}

如果讀者想進一步刨根問底,可以研究一下傳遞給 name 和 ordinal 的值是什麼。這裡先給出答案:因為調用構造函數創建的枚舉常量,是由 static final 修飾的,所以調用的時機發生在類載入的初始化階段,這時編譯器會按順序收集所有 static 賦值語句和 static 塊,生成一個 <clinit> 方法,然後去執行這個方法。所以,name 和 ordinal 參數可以在該方法中分析位元組碼找到。name 實際上就是枚舉常量名,ordinal 就是枚舉常量在枚舉類中聲明的位置,從0開始計數。記錄這兩個參數是為了方便 Enum 中 toString、ordinal、compareTo 等方法的調用

能夠看到這裡,想必也就明白了枚舉類這個 JDK5 才加入的新特性,就是一顆「語法糖」罷了。為了保持向後兼容性,Java 編譯器做了很多幕後工作。根據這樣的思路,我們也可以探究一下其他 Java 語法糖是如何實現的,比如 forEach 方法、自動裝箱/拆箱、泛型為什麼會擦除類型等

最後總結一下比較實用的 Enum 類提供的方法,和編譯器為每個枚舉類自動添加的兩個方法(values() 和 valueOf(String))

Enum 類提供的方法:

  • String name():等同於 toString()
  • int ordinal():返回枚舉常量在枚舉類中聲明的位置,從0開始計數
  • String toString():返回枚舉常量名
  • int compareTo(E other):比較兩個枚舉常量聲明的位置誰更靠前,其實靠的就是比較 ordinal 的大小
  • static Enum valueOf(Class clz, String name):根據枚舉類和枚舉常量名,返回特定的枚舉常量

編譯器為每個枚舉類自動添加的方法:

  • Enum[] values():返回枚舉常量數組,實際上編譯器在 <clinit> 中創建各個枚舉常量時,也會創建一個欄位 $VALUES,其中就保存了這個數組
  • Enum valueOf(String name):根據枚舉常量名,返回該枚舉類中特定的枚舉常量,內部調用的就是 Enum 的 valueOf 方法