有些类型的数据是可枚举的,比如一年的春夏秋冬四季、一年的十二个月、一周的七天,以及应用中其它可枚举的数据。有些代码中习惯使用常量来表示这些可枚举的数据,分为以下两种:
- int常量
- String常量
1 | public static final int APPLE_FUJI =0; |
使用int常量,具有以下缺点:
- 没有强制分组,一个文件内可能存在多组数据,影响可读性和使用。
- int值是编译时常量(compile-time constants),当值改变时,客户端如果没有重新编译,仍然可以正常运行,但是行为却变化了。
- 不可打印,打印出的int值,无法直接表示数据的含义。
- 不可遍历,没有办法在组内遍历所有枚举数据。
String常量与int常量类似,虽然在可打印性这方面好于int常量,但却更不值得考虑。因为:
- 比较String类型的数据值可能导致性能问题。
- 容易引导调用方使用硬编码(hard-code)字符串,而字符串的拼写错误在编译时是无法发现的,这将导致bug。
所以,应当枚举代替常量表示枚举型数据。
1 | public enum Apple { FUJI, PIPPIN, GRANNY_SMITH } |
Java枚举类型
在C,C++,和C#中,枚举只是一个int常量。而Java枚举类,更加强大。enum这个关键字实际上是一个语法糖,编译器对其解析的结果,实际上是一个继承了Enum的类,并且实现了许多方法。下面的两段代码,第一段是定义了一个枚举。第二段代码是这个枚举的字节码文件,反编译后的一种结果。
以下为源代码:
1 |
|
以下为反编译后的结果:
1 |
|
它的基本思路是,为每一个枚举常量,通过一个公有静态常量字段,提供唯一的一个实例。枚举类型都是常量,并且没有可访问的构造器。使用枚举的时候,既不能创建新的实例,又不能继承这个枚举,所以,只能使用枚举创建好的这些实例。换句话说,枚举是对实例可控的(instance-controlled)。枚举是广义上的单例。在我的上一篇文章中介绍了实现单例的几种方式,并且说明只有枚举能够防止反射攻击。由于enum是一个语法糖,隐藏了代码的细节。所以枚举在语法级别防止反射攻击。实际上,枚举满足除了延迟加载之外的所有要求:
- 反射攻击(防止通过反射调用私有属性或方法,生成新的实例)
- 反序列化问题(防止多次反序列化生成多个不同的实例)
- 线程安全(防止不同线程生成多个不同的实例)
由于枚举实际上是一个继承自Enum的类,而Enum实现了readObject方法,在反序列化时会抛出异常,所以可以防止多次反序列化生成多个不同的实例。而由于枚举变量在编译之后实际上是static final,所以可以保证多个线程只会创建一个实例。但是需要注意的是,我们在枚举中自己添加的方法并不是线程安全的,需要自己处理这个问题。更多单例相关的知识,可以查看我的我的上一篇文章。
枚举的其它好处
- 枚举提供了编译时类型检查,如果你定义了一个类型为Apple的参数,可以保证传入的值要么是null,要么是三个有效值中的一个。
- 两个不同类型的枚举,可以定义相同名称的枚举常量,因为每个枚举类型有自己的命名空间(这个很好理解,每个类都有自己的命名空间)。
- 可以在枚举中添加常量,或者改变常量的顺序,而不必重新编译使用方的代码。因为枚举将枚举常量导出的那段代码,使得枚举和客户端代码绝缘,枚举的值并不会像上面说的int常量那样编译到客户端中。
- 可以调用toString方法,很方便地打印枚举实例值。
- 与int常量相比,枚举类型可以随意地添加方法、字段以及实现接口。高质量地实现了Object类的所有方法,并且实现了Comparable和Serializable接口,对Serializable的实现设计成可以容忍枚举类型的大多数改变。
枚举类,总体来说,性能和int常量相当。只有在加载和初始化的时候会有一些开销,但这在大多数情况下是可以忽略的。
什么时候使用枚举
什么时候需要使用枚举呢?答案是需要使用固定的常量集合任何时候。既包含一年四季,一周七天这种自然枚举,也包含在编译时就知道所有可能值的其它情况,比如加减乘除等运算。枚举值不必是固定的,可以在后面继续添加。
与int常量相比,枚举的优点是很明显的。枚举更具可读性、更安全,并且功能更强大。许多枚举不需要明显的构造器和成员变量,而有些枚举通过为每个枚举值绑定数据,并且为这些数据提供函数,可以添加额外的功能。