什麼是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);
}
調用的該方法的時候