高效Java技巧之尽可能使用枚举代替常量

有些类型的数据是可枚举的,比如一年的春夏秋冬四季、一年的十二个月、一周的七天,以及应用中其它可枚举的数据。有些代码中习惯使用常量来表示这些可枚举的数据,分为以下两种:

  • int常量
  • String常量
1
2
3
4
5
6
public static final int APPLE_FUJI =0;
public static final int APPLE_PIPPIN =1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;

使用int常量,具有以下缺点:

  • 没有强制分组,一个文件内可能存在多组数据,影响可读性和使用。
  • int值是编译时常量(compile-time constants),当值改变时,客户端如果没有重新编译,仍然可以正常运行,但是行为却变化了。
  • 不可打印,打印出的int值,无法直接表示数据的含义。
  • 不可遍历,没有办法在组内遍历所有枚举数据。

String常量与int常量类似,虽然在可打印性这方面好于int常量,但却更不值得考虑。因为:

  • 比较String类型的数据值可能导致性能问题。
  • 容易引导调用方使用硬编码(hard-code)字符串,而字符串的拼写错误在编译时是无法发现的,这将导致bug。

所以,应当枚举代替常量表示枚举型数据。

1
2
public enum Apple  { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }

Java枚举类型

在C,C++,和C#中,枚举只是一个int常量。而Java枚举类,更加强大。enum这个关键字实际上是一个语法糖,编译器对其解析的结果,实际上是一个继承了Enum的类,并且实现了许多方法。下面的两段代码,第一段是定义了一个枚举。第二段代码是这个枚举的字节码文件,反编译后的一种结果。
以下为源代码:

1
2
3
4
5

public enum TantanitLogoEnum {
singleton
}

以下为反编译后的结果:

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

/*
* Decompiled with CFR 0_121.
*/
package com.tantanit.effective;

public final class TantanitLogoEnum
extends Enum<TantanitLogoEnum> {
public static final /* enum */ TantanitLogoEnum singleton = new TantanitLogoEnum();
private static final /* synthetic */ TantanitLogoEnum[] $VALUES;

public static TantanitLogoEnum[] values() {
return (TantanitLogoEnum[])$VALUES.clone();
}

public static TantanitLogoEnum valueOf(String name) {
return Enum.valueOf(TantanitLogoEnum.class, name);
}

private TantanitLogoEnum() {
super(string, n);
}

static {
$VALUES = new TantanitLogoEnum[]{singleton};
}
}


它的基本思路是,为每一个枚举常量,通过一个公有静态常量字段,提供唯一的一个实例。枚举类型都是常量,并且没有可访问的构造器。使用枚举的时候,既不能创建新的实例,又不能继承这个枚举,所以,只能使用枚举创建好的这些实例。换句话说,枚举是对实例可控的(instance-controlled)。枚举是广义上的单例。在我的上一篇文章中介绍了实现单例的几种方式,并且说明只有枚举能够防止反射攻击。由于enum是一个语法糖,隐藏了代码的细节。所以枚举在语法级别防止反射攻击。实际上,枚举满足除了延迟加载之外的所有要求:

  • 反射攻击(防止通过反射调用私有属性或方法,生成新的实例)
  • 反序列化问题(防止多次反序列化生成多个不同的实例)
  • 线程安全(防止不同线程生成多个不同的实例)

由于枚举实际上是一个继承自Enum的类,而Enum实现了readObject方法,在反序列化时会抛出异常,所以可以防止多次反序列化生成多个不同的实例。而由于枚举变量在编译之后实际上是static final,所以可以保证多个线程只会创建一个实例。但是需要注意的是,我们在枚举中自己添加的方法并不是线程安全的,需要自己处理这个问题。更多单例相关的知识,可以查看我的我的上一篇文章

枚举的其它好处

  • 枚举提供了编译时类型检查,如果你定义了一个类型为Apple的参数,可以保证传入的值要么是null,要么是三个有效值中的一个。
  • 两个不同类型的枚举,可以定义相同名称的枚举常量,因为每个枚举类型有自己的命名空间(这个很好理解,每个类都有自己的命名空间)。
  • 可以在枚举中添加常量,或者改变常量的顺序,而不必重新编译使用方的代码。因为枚举将枚举常量导出的那段代码,使得枚举和客户端代码绝缘,枚举的值并不会像上面说的int常量那样编译到客户端中。
  • 可以调用toString方法,很方便地打印枚举实例值。
  • 与int常量相比,枚举类型可以随意地添加方法、字段以及实现接口。高质量地实现了Object类的所有方法,并且实现了Comparable和Serializable接口,对Serializable的实现设计成可以容忍枚举类型的大多数改变。

枚举类,总体来说,性能和int常量相当。只有在加载和初始化的时候会有一些开销,但这在大多数情况下是可以忽略的。

什么时候使用枚举

什么时候需要使用枚举呢?答案是需要使用固定的常量集合任何时候。既包含一年四季,一周七天这种自然枚举,也包含在编译时就知道所有可能值的其它情况,比如加减乘除等运算。枚举值不必是固定的,可以在后面继续添加。

与int常量相比,枚举的优点是很明显的。枚举更具可读性、更安全,并且功能更强大。许多枚举不需要明显的构造器和成员变量,而有些枚举通过为每个枚举值绑定数据,并且为这些数据提供函数,可以添加额外的功能。

© 2022 谈谈IT All Rights Reserved. 本站访客数人次 本站总访问量
Theme by hiero