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

酒冽發表於2022-01-27

列舉類的使用

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

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;
}
作者:酒冽        出處:https://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$1Color$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 的構造器額外新增兩個方法引數:Stringint 型呢?

來看看 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;
}

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

能夠看到這裡,想必也就明白了列舉類這個 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):根據列舉常量名,返回該列舉類中特定的列舉常量,內部呼叫的就是 EnumvalueOf 方法

相關文章