深入理解Java枚舉

深入理解Java枚舉

重新認識Java枚舉

老實說,挺羞愧的,這麼久了,一直不知道Java枚舉的本質是啥,雖然也在用,但是真不知道它的底層是個啥樣的

直到2020年4月28日的晚上20點左右,我才真的揭開了Java枚舉的面紗,看到了它的真面目,但是我哭了

這篇文章不是深入理解枚舉,而是認識枚舉,是的,因爲之前都是陌路人

緣起

在幾個月以前,遇到需要自定義一個mybatis枚舉類型的TypeHandler,當時有多個枚舉類型,想寫一個Handler搞定的,實踐中發現,這些枚舉類型得有一個共同的父類,才能實現,缺父類?沒問題,給它們安排上!

創建好父類,讓小崽子們來認父?

然而,我以爲小崽子沒有爸爸的,誰知道編譯器告訴我,它已經有了爸爸!!!

那就是java.lang.Enum這個類,它是一個抽象類,其Java Doc明確寫到

This is the common base class of all Java language enumeration types.

當時也沒在意,有就有了,有了還得我麻煩了。

前兩天羣裏有個人問,說重寫了枚舉類的toString方法,怎麼沒有生效呢?

先是懷疑他哪裏沒搞對,不可能重寫toString不起作用的。

我的第一動作是進行自洽解釋,從結果去推導原因

這是大忌,代碼的事情,就讓代碼來說

給出了一個十分可笑的解釋

枚舉類裏的枚舉常量是繼承自java.lang.Enum,而你重寫的是枚舉類的toString(),是java.lang.ObjecttoString()被重寫了,所以不起作用

還別說,我當時還挺高興的,發現一個知識盲點,打算寫下來,現在想來,那不是盲點,是瞎了

不過雖然想把上面的知識盲點寫下來,但是還是有些好奇,想弄明白怎麼回事

因爲當時討論的時候,我好像提到過java.lang.Enum是Java中所有枚舉類的父類,當時說到了是在編譯器,給它整個爸爸的,所以想看看一個枚舉類編譯後是什麼樣的。

這一看不當緊,才知道當時說那話是多麼的可笑

頓悟

廢話不多說,上澀圖

圖一

上圖是枚舉類Java源代碼


下圖是上圖編譯後的Class文件反編譯後的

javap -c classFilePath

圖二

反編譯後的內容可能很多人都看不懂,我也不咋懂,不過我們主要看前面幾行就差不多了。

第一行就是表明父子關係的類繼承,這裏就證實,編譯器做了手腳的,強行給enum修飾的的類安排了一個爸爸

下面幾行就有意思了

  public static final com.example.demo.enu.DemoEnum ONE;

  public static final com.example.demo.enu.DemoEnum TWO;

  public static final com.example.demo.enu.DemoEnum THREE;

  int num;

然後就很容易想到這個

   ONE(1),
  TWO(2),
  THREE(3);
  int num;

是多麼多麼多麼的相似!

可以看到,我們在Java源碼中寫的ONE(1) 在編譯後的實際上是一個DemoEnum類型的常量

ONE == public static final com.example.demo.enu.DemoEnum ONE

編譯器幫我們做了這個操作

也就是說我們所寫的枚舉類,其實可以這麼來寫,效果等同

public class EqualEnum {

    public static final EqualEnum ONE = new EqualEnum(1);
    public static final EqualEnum TWO = new EqualEnum(2);
    public static final EqualEnum THREE = new EqualEnum(3);

    int num ;

    public EqualEnum (int num) {
        this.num = num;
    }
}

這個普通的的Java類,和我們上面寫的

public enum DemoEnum {
   ONE(1),
   TWO(2),
   THREE(3);
   int num;
   DemoEnum (int num) {
       this.num = num;
   }
}

它們真的一樣啊,哇槽!

這個同時也解釋了我的一個疑問

爲啥我枚舉類型,如果想表示別的信息數據時,一定要有相應的成員變量,以及一個對應的構造器?

這個構造器誰來調用呢?

它來調用,這個靜態塊的內容實際上就是<clinit>構造器的內容

Tps: 之前分不清類初始化構造器,和實例初始化構造器,可以這麼理解 可以理解爲classloadInit,類構造器在類加載的過程中被調用,而則是初始化一個對象的。

 static {};
    Code:
       // 創建一個DemoEnum對象
       0: new           #4                  // class com/example/demo/enu/DemoEnum
       // 操作數棧頂複製並且入棧
       3: dup
       // 把String ONE 入棧
       4: ldc           #14                 // String ONE
       // int常量值0入棧
       6: iconst_0
       7: iconst_1
       // 調用實例初始化方法
       8: invokespecial #15                 // Method "<init>":(Ljava/lang/String;II)V
      // 對類成員變量ONE賦值
      11: putstatic     #16                 // Field ONE:Lcom/example/demo/enu/DemoEnum;
      // 下面兩個分別是初始化TWO 和THREE的,過程一樣
      14: new           #4                  // class com/example/demo/enu/DemoEnum
      17: dup
      18: ldc           #17                 // String TWO
      20: iconst_1
      21: iconst_2
      22: invokespecial #15                 // Method "<init>":(Ljava/lang/String;II)V
      25: putstatic     #18                 // Field TWO:Lcom/example/demo/enu/DemoEnum;
      28: new           #4                  // class com/example/demo/enu/DemoEnum
      31: dup
      32: ldc           #19                 // String THREE
      34: iconst_2
      35: iconst_3
      36: invokespecial #15                 // Method "<init>":(Ljava/lang/String;II)V
      39: putstatic     #20                 // Field THREE:Lcom/example/demo/enu/DemoEnum;
      42: iconst_3
      // 這裏是新建一個DemoEnum類型的數組
      // 推測是直接在棧頂的
      43: anewarray     #4                  // class com/example/demo/enu/DemoEnum
      46: dup
      47: iconst_0
      // 獲取Field ONE,
      48: getstatic     #16                 // Field ONE:Lcom/example/demo/enu/DemoEnum;
      // 存入數組中
      51: aastore
      52: dup
      53: iconst_1
      // 獲取 Field TWO 
      54: getstatic     #18                 // Field TWO:Lcom/example/demo/enu/DemoEnum;
      // 存入數組
      57: aastore
      58: dup
      59: iconst_2
      // 獲取Field THREE 
      60: getstatic     #20                 // Field THREE:Lcom/example/demo/enu/DemoEnum;
      // 存入數組
      63: aastore
      // 棧頂元素 賦值給Field DemoEnum[] $VALUES
      64: putstatic     #1                  // Field $VALUES:[Lcom/example/demo/enu/DemoEnum;
      67: return
}

這就是爲啥需要對應的有參構造器的原因

到這裏還是存有一些疑問

我們定義了一個枚舉類,肯定是需要拿來使用的,尤其是當我們的枚舉類還有一些其他有意義的字段的時候

比如我們上面的例子ONE(1),通過1這個數值,去獲得枚舉值 ONE,這是很常見的一個需求。

方式也很簡單

DemoEnum[] vals = DemoEnum.values()
for(int i=0; i< vals.length; i++){
  if(vals[i].num == 1){
    return vals[i];
  }
}

通過上面就可以找到枚舉值ONE

可是找遍了我們自己寫的枚舉類DemoEnum和它的強行安排的父類Enum,都沒有找到靜態方法values

如果你細心的看到這裏,應該是能明白的

我們上面通過分析反編譯後的字節碼,看到兩處可疑目標

下面這段在開始的截圖有出現

 public static com.example.demo.enu.DemoEnum[] values();
    Code:
       // 獲取靜態域 $VALUES的值
       0: getstatic     #1  // Field $VALUES:[Lcom/example/demo/enu/DemoEnum;
       // 調用clone()方法
       3: invokevirtual #2	// Method "[Lcom/example/demo/enu/DemoEnum;".clone:()Ljava/lang/Object;
       // 類型檢查
       6: checkcast     #3  // class "[Lcom/example/demo/enu/DemoEnum;"
       // 返回clone()後的方法
       9: areturn

上面之所以要使用clone(),是避免調用values(),將內部的數組暴露出去,從而有被修改的分險,也存在線程安全問題

後面一處,就是在static{}塊最後那部分

從這兩處反編譯後的字節碼,我們能很清晰明瞭的知道這個套路了

編譯器自己給我們強行插入一個靜態方法values(),而且還有一個 T[] $VALUES數組,不過這個靜態域在源碼沒找到,估計是編譯器編譯時加進去的

到這裏還沒完,我們再來看個有意思的java.lang.Class#getEnumConstantsShared,在java.lang.Class中有這麼個方法,訪問修飾符是default,包訪問級別的

T[] getEnumConstantsShared() {
    if (enumConstants == null) {
        if (!isEnum()) return null;
        try {
            // 看這裏 看這裏 看這裏
            final Method values = getMethod("values");
            java.security.AccessController.doPrivileged(
                new java.security.PrivilegedAction<Void>() {
                    public Void run() {
                            values.setAccessible(true);
                            return null;
                        }
                    });
            @SuppressWarnings("unchecked")
            // 還有這裏 這裏 這裏
            T[] temporaryConstants = (T[])values.invoke(null);
            enumConstants = temporaryConstants;
        }
        // These can happen when users concoct enum-like classes
        // that don't comply with the enum spec.
        // 這裏是一個安全保護,防止自己寫了一個類似enum的類,但是沒有values方法
        catch (InvocationTargetException | NoSuchMethodException |
               IllegalAccessException ex) { return null; }
    }
    return enumConstants;
}

我們的valuesOf方法,在底層就是調用它來實現的,很遺憾的是,這個valuesOf方法,僅僅實現了通過枚舉類型的name來查找對應的枚舉值。

也就是我們只能通過變量名 name = "ONE"這種方式,來查找到DemoEnum.ONE這個枚舉值

後記

以前因爲枚舉用的少,也就僅僅停留在使用的層面,其實在使用的過程中,也有很多疑惑產生,但是並沒有真正像現在這樣去深究它的實現。

也許是之前動力不足,也許是對未知的恐懼,也許是其他方面的知識準備還不夠。

總之,到現在纔算真的理解Java枚舉

關於其他方面的知識準備不足,這個我覺得還是值得說一下的,之前我就寫過一次說這個事的,因爲有些知識點,它並不是孤立的,是網狀的,我們在看某一個點的時候,往往就像在一個蜘蛛網上,但是這個網上太多我們不知道的東西了,所以就很容易出現去不斷的補充和它相關的知識點的情況,這個時候就會很累,而且,你最開始想學的那個知識點,也沒怎麼搞懂。

我也不知道這種方式對不對,對我來說,我是這樣做的,其實不利於快速吸收知識,但是長久下來,會讓自己的廣度拓展開來,並且遇到一些新的知識點的時候,可以更容易理解它。

拿這次決定看反編譯的字節碼這個事,如果放在一個月前,我是不敢的,真的不敢,看不懂,頭大,不會有這個想法的。

前段時間想把Java的動態代理搞一搞,很多框架都用了動態代理,不整明白,看源碼很糊塗。

因此決定看看,然後找到了樑飛關於在設計Dubbo時對動態代理的選擇的一篇文章,裏面貼出了幾種動態代理生成的字節碼的對比,看不到懂,滿腦子問號。

後來決定,瞭解下字節碼吧,把《深入理解Java虛擬機》這本書翻出來,翻到最後的附錄部分,看了一遍

初看雖然很多,但是共性很大,實際的那些操作碼並不是很多,多記幾遍就可以了

我喜歡這種明瞭的感覺,雖然快感後是索然無味,不過這也能正向激勵去不斷的探索未知,而不是因爲恐懼而退卻!

一覽無餘的感覺真爽!

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