從新認識Java枚舉java
老實說,挺羞愧的,這麼久了,一直不知道Java枚舉的本質是啥,雖然也在用,可是真不知道它的底層是個啥樣的數組
直到2020年4月28日的晚上20點左右,我才真的揭開了Java枚舉的面紗,看到了它的真面目,可是我哭了安全
在幾個月之前,遇到須要自定義一個mybatis
枚舉類型的TypeHandler
,當時有多個枚舉類型,想寫一個Handler
搞定的,實踐中發現,這些枚舉類型得有一個共同的父類,才能實現,缺父類?沒問題,給它們安排上!mybatis
建立好父類,讓小崽子們來認父?app
然而,我覺得小崽子沒有爸爸的,誰知道編譯器告訴我,它已經有了爸爸!!!框架
那就是java.lang.Enum
這個類,它是一個抽象類,其Java Doc明確寫到this
This is the common base class of all Java language enumeration types.
當時也沒在乎,有就有了,有了還得我麻煩了。線程
前兩天羣裏有我的問,說重寫了枚舉類的toString
方法,怎麼沒有生效呢?設計
先是懷疑他哪裏沒搞對,不可能重寫toString
不起做用的。代理
個人第一動做是進行自洽解釋,從結果去推導緣由
這是大忌,代碼的事情,就讓代碼來講
給出了一個十分好笑的解釋
枚舉類裏的枚舉常量是繼承自java.lang.Enum,而你重寫的是枚舉類的toString(),是java.lang.Object的toString()被重寫了,因此不起做用
還別說,我當時還挺高興的,發現一個知識盲點
,打算寫下來,如今想來,那不是盲點,是瞎了
不過雖然想把上面的知識盲點
寫下來,可是仍是有些好奇,想弄明白怎麼回事
由於當時討論的時候,我好像提到過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虛擬機》
這本書翻出來,翻到最後的附錄部分,看了一遍
初看雖然不少,可是共性很大,實際的那些操做碼並非不少,多記幾遍就能夠了
我喜歡這種明瞭的感受,雖然快感後是索然無味
,不過這也能正向激勵去不斷的探索未知,而不是由於恐懼而退卻!
盡收眼底的感受真爽!