深入理解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.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虛擬機》
這本書翻出來,翻到最後的附錄部分,看了一遍
初看雖然很多,但是共性很大,實際的那些操作碼並不是很多,多記幾遍就可以了
我喜歡這種明瞭的感覺,雖然快感後是索然無味
,不過這也能正向激勵去不斷的探索未知,而不是因爲恐懼而退卻!
一覽無餘的感覺真爽!