關於Java中列舉Enum的深入剖析
在程式語言中我們,都會接觸到列舉型別,通常我們進行有窮的列舉來實現一些限定。Java也不例外。Java中的列舉型別為Enum,本文將對列舉進行一些比較深入的剖析。
什麼是Enum
Enum是自Java 5 引入的特性,用來方便Java開發者實現列舉應用。一個簡單的Enum使用如下。
// ColorEnum.java public enum ColorEmun { RED, GREEN, YELLOW } public void setColorEnum(ColorEmun colorEnum) { //some code here } setColorEnum(ColorEmun.GREEN);
為什麼會有Enum
在Enum之前的我們使用類似如下的程式碼實現列舉的功能.
public static final int COLOR_RED = 0; public static final int COLOR_GREEN = 1; public static final int COLOR_YELLOW = 2; public void setColor(int color) { //some code here } //呼叫 setColor(COLOR_RED)
然而上面的還是有不盡完美的地方
- setColor(COLOR_RED)與setColor(0)效果一樣,而後者可讀性很差,但卻可以正常執行
- setColor方法可以接受列舉之外的值,比如setColor(3),這種情況下程式可能出問題
概括而言,傳統列舉有如下兩個弊端
- 安全性
- 可讀性,尤其是列印日誌時
因此Java引入了Enum,使用Enum,我們實現上面的列舉就很簡單了,而且還可以輕鬆避免傳入非法值的風險.
列舉原理是什麼
Java中Enum的本質其實是在編譯時期轉換成對應的類的形式。
首先,為了探究列舉的原理,我們先簡單定義一個列舉類,這裡以季節為例,類名為Season
,包含春夏秋冬四個列舉條目.
public enum Season { SPRING, SUMMER, AUTUMN, WINTER }
然後我們使用javac編譯上面的類,得到class檔案.
javac Season.java
然後,我們利用反編譯的方法來看看位元組碼檔案究竟是什麼.這裡使用的工具是javap的簡單命令,先列舉一下這個Season下的全部元素.
company javap Season Warning: Binary file Season contains com.company.Season Compiled from "Season.java" public final class com.company.Season extends java.lang.Enum<com.company.Season> { public static final com.company.Season SPRING; public static final com.company.Season SUMMER; public static final com.company.Season AUTUMN; public static final com.company.Season WINTER; public static com.company.Season[] values(); public static com.company.Season valueOf(java.lang.String); static {}; }
從上反編譯結果可知
- java程式碼中的Season轉換成了繼承自的java.lang.enum的類
- 既然隱式繼承自java.lang.enum,也就意味java程式碼中,Season不能再繼承其他的類
- Season被標記成了final,意味著它不能被繼承
static程式碼塊
使用javap具體反編譯class檔案,得到靜態程式碼塊相關的結果為
static {}; Code: 0: new #4 // class com/company/Season 3: dup 4: ldc #7 // String SPRING 6: iconst_0 7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V 10: putstatic #9 // Field SPRING:Lcom/company/Season; 13: new #4 // class com/company/Season 16: dup 17: ldc #10 // String SUMMER 19: iconst_1 20: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V 23: putstatic #11 // Field SUMMER:Lcom/company/Season; 26: new #4 // class com/company/Season 29: dup 30: ldc #12 // String AUTUMN 32: iconst_2 33: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V 36: putstatic #13 // Field AUTUMN:Lcom/company/Season; 39: new #4 // class com/company/Season 42: dup 43: ldc #14 // String WINTER 45: iconst_3 46: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V 49: putstatic #15 // Field WINTER:Lcom/company/Season; 52: iconst_4 53: anewarray #4 // class com/company/Season 56: dup 57: iconst_0 58: getstatic #9 // Field SPRING:Lcom/company/Season; 61: aastore 62: dup 63: iconst_1 64: getstatic #11 // Field SUMMER:Lcom/company/Season; 67: aastore 68: dup 69: iconst_2 70: getstatic #13 // Field AUTUMN:Lcom/company/Season; 73: aastore 74: dup 75: iconst_3 76: getstatic #15 // Field WINTER:Lcom/company/Season; 79: aastore 80: putstatic #1 // Field $VALUES:[Lcom/company/Season; 83: return }
其中
- 0~52為例項化SPRING, SUMMER, AUTUMN, WINTER
- 53~83為建立
Season[]
陣列$VALUES
,並將上面的四個物件放入陣列的操作.
values方法
values方法的的返回值實際上就是上面$VALUES
陣列物件
swtich中的列舉
在Java中,switch-case是我們經常使用的流程控制語句.當列舉出來之後,switch-case也很好的進行了支援.
比如下面的程式碼是完全正常編譯,正常執行的.
public static void main(String[] args) { Season season = Season.SPRING; switch(season) { case SPRING: System.out.println("It's Spring"); break; case WINTER: System.out.println("It's Winter"); break; case SUMMER: System.out.println("It's Summer"); break; case AUTUMN: System.out.println("It's Autumn"); break; } }
不過,通常情況下switch-case支援類似int的型別,那麼它是怎麼做到對Enum的支援呢,我們反編譯上述方法看一下位元組碼的真實情況.
public static void main(java.lang.String[]); Code: 0: getstatic #2 // Field com/company/Season.SPRING:Lcom/company/Season; 3: astore_1 4: getstatic #3 // Field com/company/Main$1.$SwitchMap$com$company$Season:[I 7: aload_1 8: invokevirtual #4 // Method com/company/Season.ordinal:()I 11: iaload 12: tableswitch { // 1 to 4 1: 44 2: 55 3: 66 4: 77 default: 85 } 44: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 47: ldc #6 // String It's Spring 49: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 52: goto 85 55: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 58: ldc #8 // String It's Winter 60: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 63: goto 85 66: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 69: ldc #9 // String It's Summer 71: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 74: goto 85 77: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 80: ldc #10 // String It's Autumn 82: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 88: return
注意上面程式碼塊有這樣的一段程式碼
8: invokevirtual #4 // Method com/company/Season.ordinal:()I
事實果真如此,在switch-case中,還是將Enum轉成了int值(通過呼叫Enum.oridinal()方法)
列舉與混淆
在Android開發中,進行混淆是我們在釋出前必不可少的工作,混下後,我們能增強反編譯的難度,在一定程度上保護了增強了安全性.
而開發人員處理混淆更多的是將某些元素加入不混淆的名單,這裡列舉就是需要排除混淆的.
在預設的混淆配置檔案中,已經加入了關於對列舉混淆的處理
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations -keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); }
關於為什麼要保留values()方法和valueOf()方法,請參考文章讀懂 Android 中的程式碼混淆 關於列舉的部分
使用proguard優化
使用Proguard進行優化,可以將列舉儘可能的轉換成int。配置如下
-optimizations class/unboxing/enum
確保上述程式碼生效,需要確proguard配置檔案不包含-dontoptimize
指令。
當我們使用gradlew打包是,看到類似下面的輸出,即Number of unboxed enum classes:1
代表已經將一個列舉轉換成了int的形式。
Optimizing... Number of finalized classes: 0 (disabled) Number of unboxed enum classes: 1 Number of vertically merged classes: 0 (disabled) Number of horizontally merged classes: 0 (disabled)
列舉單例
單例模式是我們在日常開發中可謂是最常用的設計模式.
然後要設計好單例模式,無非考慮一下幾點
- 確保只有唯一例項,不多建立多餘例項
- 確保例項按需建立.
因此傳統的做法想要實現單例,大致有一下幾種
- 餓漢式載入
- 懶漢式synchronize和雙重檢查
- 利用java的靜態載入機制
相比上述的方法,使用列舉也可以實現單例,而且還更加簡單.
public enum AppManager { INSTANCE; private String tagName; public void setTag(String tagName) { this.tagName = tagName; } public String getTag() { return tagName; } }
呼叫起來也更加簡單
AppManager.INSTANCE.getTag();
列舉如何確保唯一例項
因為獲得例項只能通過AppManager.INSTANCE
下面的方式是不可以的
AppManager appManager = new AppManager(); //compile error
關於單例模式,可以閱讀單例這種設計模式瞭解更多。
(Android中)該不該用列舉
既然上面提到了列舉會轉換成類,這樣理論上造成了下面的問題
- 增加了dex包的大小,理論上dex包越大,載入速度越慢
- 同時使用列舉,執行時的記憶體佔用也會相對變大
關於上面兩點的驗證,秋百萬已經做了詳細的論證,大家可以參考這篇文章《Android 中的 Enum 到底佔多少記憶體?該如何用?》
關於列舉是否使用的結論,大家可以參考
- 如果你開發的是Framework不建議使用enum
- 如果是簡單的enum,可以使用int很輕鬆代替,則不建議使用enum
- 另外,如果是Android中,可以使用下面介紹的列舉註解來實現。
- 除此之外,我們還需要對比可讀性和易維護性來與效能進行衡量,從中進行做出折中
在Android中的替代
Android中新引入的替代列舉的註解有IntDef和StringDef,這裡以IntDef做例子說明一下.
public class Colors { @IntDef({RED, GREEN, YELLOW}) @Retention(RetentionPolicy.SOURCE) public @interface LightColors{} public static final int RED = 0; public static final int GREEN = 1; public static final int YELLOW = 2; }
- 宣告必要的int常量
- 宣告一個註解為LightColors
- 使用@IntDef修飾LightColors,引數設定為待列舉的集合
- 使用@Retention(RetentionPolicy.SOURCE)指定註解僅存在與原始碼中,不加入到class檔案中
比如我們用來標註方法的引數
private void setColor(@Colors.LightColors int color) { Log.d("MainActivity", "setColor color=" + color); }
呼叫的該方法的時候
setColor(Colors.GREEN);
關於Android中的列舉,可以參考探究Android中的註解
以上就是我對Java中enum的一些深入的剖析,歡迎大家不吝賜教。
相關文章
- Java 列舉(enum)Java
- 再談java列舉enumJava
- Java - Enum 列舉型別Java型別
- Java 列舉 enum 詳解Java
- Java —— 列舉類(enum 類)Java
- 聊一聊Java的列舉enumJava
- Java enum列舉類詳解 列舉的常見用法Java
- 聊聊TypeScript中列舉物件(Enum)TypeScript物件
- TypeScript 列舉enumTypeScript
- Java列舉型別enum的詳解及使用Java型別
- 【python】Enum 列舉類Python
- ENUM列舉型別型別
- Rust的列舉型別EnumRust型別
- Java列舉enum可以有抽象方法! -RecepİnançJava抽象NaN
- Java列舉:小小enum,優雅而乾淨Java
- Java 列舉(enum) 詳解7種常見的用法Java
- 深入淺出 Java 中列舉的實現原理Java
- 深入理解 Java 列舉Java
- java中的列舉Java
- 從JDK角度認識列舉enumJDK
- java中的列舉型別Java型別
- 使用列舉ENUM替換Switch或If-Else
- 優雅使用前端列舉Enum,符合國標的那種!前端
- java中的列舉型別學習Java型別
- Java列舉Java
- Java 列舉、JPA 和 PostgreSQL 列舉JavaSQL
- PLC結構化文字(ST)——隱式列舉(implicit enum)
- Enum列舉型別實戰總結,保證有用!型別
- 位元組碼層面深入分析Java列舉類Java
- 資料結構複習-01enum列舉型別資料結構型別
- Java 列舉 switch的用法Java
- java列舉類Java
- Java列舉-通過值查詢對應的列舉Java
- 【C/C++】C和C++11之enum列舉的使用細節C++
- 【java基礎】列舉Java
- Java列舉解讀Java
- 【java】【列舉使用技巧】Java
- Java(4)列舉類Java
- Java基礎--列舉Java