在编程语言中我们,都会接触到枚举类型,通常我们进行有穷的列举来实现一些限定。Java也不例外。Java中的枚举类型为Enum,本文将对枚举进行一些比较深入的剖析。
什么是Enum
Enum是自Java 5 引入的特性,用来方便Java开发者实现枚举应用。一个简单的Enum使用如下。
1
2
3
4
5
6
7
8
9
10
11
12
// ColorEnum.java
public enum ColorEmun {
RED ,
GREEN ,
YELLOW
}
public void setColorEnum ( ColorEmun colorEnum ) {
//some code here
}
setColorEnum ( ColorEmun . GREEN );
为什么会有Enum
在Enum之前的我们使用类似如下的代码实现枚举的功能.
1
2
3
4
5
6
7
8
9
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
,包含春夏秋冬四个枚举条目.
1
2
3
4
5
6
public enum Season {
SPRING ,
SUMMER ,
AUTUMN ,
WINTER
}
然后我们使用javac编译上面的类,得到class文件.
然后,我们利用反编译的方法来看看字节码文件究竟是什么.这里使用的工具是javap的简单命令,先列举一下这个Season下的全部元素.
1
2
3
4
5
6
7
8
9
10
11
12
➜ 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文件,得到静态代码块相关的结果为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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也很好的进行了支持.
比如下面的代码是完全正常编译,正常运行的.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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的支持呢,我们反编译上述方法看一下字节码的真实情况.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
注意上面代码块有这样的一段代码
1
8 : invokevirtual # 4 // Method com/company/Season.ordinal:()I
事实果真如此,在switch-case中,还是将Enum转成了int值(通过调用Enum.oridinal()方法)
枚举与混淆
在Android开发中,进行混淆是我们在发布前必不可少的工作,混下后,我们能增强反编译的难度,在一定程度上保护了增强了安全性.
而开发人员处理混淆更多的是将某些元素加入不混淆的名单,这里枚举就是需要排除混淆的.
在默认的混淆配置文件中,已经加入了关于对枚举混淆的处理
1
2
3
4
5
# 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。配置如下
1
- optimizations class / unboxing / enum
确保上述代码生效,需要确proguard配置文件不包含-dontoptimize
指令。
当我们使用gradlew打包是,看到类似下面的输出,即Number of unboxed enum classes:1
代表已经将一个枚举转换成了int的形式。
1
2
3
4
5
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的静态加载机制
相比上述的方法,使用枚举也可以实现单例,而且还更加简单.
1
2
3
4
5
6
7
8
9
10
11
12
public enum AppManager {
INSTANCE ;
private String tagName ;
public void setTag ( String tagName ) {
this . tagName = tagName ;
}
public String getTag () {
return tagName ;
}
}
调用起来也更加简单
1
AppManager . INSTANCE . getTag ();
枚举如何确保唯一实例
因为获得实例只能通过AppManager.INSTANCE
下面的方式是不可以的
1
AppManager appManager = new AppManager (); //compile error
关于单例模式,可以阅读单例这种设计模式 了解更多。
(Android中)该不该用枚举
既然上面提到了枚举会转换成类,这样理论上造成了下面的问题
增加了dex包的大小,理论上dex包越大,加载速度越慢
同时使用枚举,运行时的内存占用也会相对变大
关于上面两点的验证,秋百万已经做了详细的论证,大家可以参考这篇文章《Android 中的 Enum 到底占多少内存?该如何用?》
关于枚举是否使用的结论,大家可以参考
如果你开发的是Framework不建议使用enum
如果是简单的enum,可以使用int很轻松代替,则不建议使用enum
另外,如果是Android中,可以使用下面介绍的枚举注解来实现。
除此之外,我们还需要对比可读性和易维护性来与性能进行衡量,从中进行做出折中
在Android中的替代
Android中新引入的替代枚举的注解有IntDef和StringDef,这里以IntDef做例子说明一下.
1
2
3
4
5
6
7
8
9
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文件中
比如我们用来标注方法的参数
1
2
3
private void setColor ( @Colors.LightColors int color ) {
Log . d ( "MainActivity" , "setColor color=" + color );
}
调用的该方法的时候
1
setColor ( Colors . GREEN );
关于Android中的枚举,可以参考探究Android中的注解
以上就是我对Java中enum的一些深入的剖析,欢迎大家不吝赐教。