1. 概述
Java 的 enum 类型为创建和使用常量提供了一种语言级别的支持。相比 String 或 int 这类字面量常量,enum 更加类型安全,因为它的取值被限定在一个有限集合中。
但 enum 的值必须是合法的标识符(identifier),并且按照惯例我们通常使用全大写的蛇形命名法(SCREAMING_SNAKE_CASE)来表示。
由于这些限制,单靠 enum 的名称无法很好地表示人类可读的字符串或非字符串类型的值。
本文将探讨如何通过将 enum 当作一个类来使用,从而为其绑定我们需要的额外值。
2. 将 Java Enum 作为类来使用
我们经常将 enum 简单地写成一组值的列表。例如,下面是周期表前两行元素的一个简单 enum:
public enum Element {
H, HE, LI, BE, B, C, N, O, F, NE
}
使用上述语法,我们创建了十个名为 Element 的静态、不可变的 enum 实例。虽然这种方式非常高效,但它只包含了元素符号。虽然大写形式符合 Java 常量命名规范,但并不符合我们日常书写元素符号的习惯。
此外,我们还缺少了元素的其他属性,如元素名称和原子量。
虽然 enum 在 Java 中有特殊的行为,但我们仍然可以像普通类一样为其添加构造函数、字段和方法。因此,我们可以通过增强 enum 来绑定我们需要的值。
3. 添加构造函数和 final 字段
我们从绑定元素名称开始。
我们通过构造函数将名称赋值给一个 final 字段:
public enum Element {
H("Hydrogen"),
HE("Helium"),
// ...
NE("Neon");
public final String label;
private Element(String label) {
this.label = label;
}
}
首先,我们注意到声明列表中的特殊语法。这是为 enum 类型调用构造函数的方式。虽然不能使用 new 操作符来创建 enum 实例,但我们可以在声明列表中传递构造函数参数。
接着我们声明了一个实例变量 label。需要注意以下几点:
*首先,我们使用 label 而不是 name 作为字段名。虽然 name 是可用的字段名,但为了避免与预定义的 Enum.name() 方法混淆,我们选择使用 label。*
其次,我们的 label 字段是 final 的。虽然 enum 的字段不一定要是 final 的,但在大多数情况下我们不希望这些标签被修改。 这与 enum 值本身不可变的语义一致。
最后,label 字段是 public 的,因此我们可以直接访问它:
System.out.println(BE.label);
当然,也可以将字段设为 private,并通过 getLabel() 方法访问。为了简洁起见,本文后续示例将使用 public 字段的方式。
4. 查找 Java Enum 值
Java 为所有 enum 类型提供了 valueOf(String) 方法。
因此,我们可以根据声明的名称获取 enum 值:
assertSame(Element.LI, Element.valueOf("LI"));
但有时我们也希望根据 label 字段来查找 enum 值。
为此,我们可以添加一个 static 方法:
public static Element valueOfLabel(String label) {
for (Element e : values()) {
if (e.label.equals(label)) {
return e;
}
}
return null;
}
这个 static 的 valueOfLabel() 方法会遍历所有 Element 值,直到找到匹配项。如果未找到,则返回 null。当然,也可以选择抛出异常而不是返回 null。
下面是一个使用 valueOfLabel() 方法的示例:
assertSame(Element.LI, Element.valueOfLabel("Lithium"));
5. 缓存查找值
为了避免每次都遍历 enum 值,我们可以使用 Map 来缓存标签。
为此,我们定义一个 static final Map,并在类加载时填充它:
public enum Element {
// ... enum values
private static final Map<String, Element> BY_LABEL = new HashMap<>();
static {
for (Element e: values()) {
BY_LABEL.put(e.label, e);
}
}
// ... fields, constructor, methods
public static Element valueOfLabel(String label) {
return BY_LABEL.get(label);
}
}
由于缓存的存在,enum 值只在类加载时遍历一次,valueOfLabel() 方法也因此简化。
作为替代方案,我们也可以在首次访问时懒加载缓存。这时,必须对 map 的访问进行同步,以避免并发问题。
6. 绑定多个值
枚举的构造函数可以接受多个参数。
为了说明这一点,我们再添加原子序号(int)和原子量(float):
public enum Element {
H("Hydrogen", 1, 1.008f),
HE("Helium", 2, 4.0026f),
// ...
NE("Neon", 10, 20.180f);
private static final Map<String, Element> BY_LABEL = new HashMap<>();
private static final Map<Integer, Element> BY_ATOMIC_NUMBER = new HashMap<>();
private static final Map<Float, Element> BY_ATOMIC_WEIGHT = new HashMap<>();
static {
for (Element e : values()) {
BY_LABEL.put(e.label, e);
BY_ATOMIC_NUMBER.put(e.atomicNumber, e);
BY_ATOMIC_WEIGHT.put(e.atomicWeight, e);
}
}
public final String label;
public final int atomicNumber;
public final float atomicWeight;
private Element(String label, int atomicNumber, float atomicWeight) {
this.label = label;
this.atomicNumber = atomicNumber;
this.atomicWeight = atomicWeight;
}
public static Element valueOfLabel(String label) {
return BY_LABEL.get(label);
}
public static Element valueOfAtomicNumber(int number) {
return BY_ATOMIC_NUMBER.get(number);
}
public static Element valueOfAtomicWeight(float weight) {
return BY_ATOMIC_WEIGHT.get(weight);
}
}
同样地,我们可以在 enum 中添加任何我们需要的值,例如规范格式的元素符号(如“He”、“Li”、“Be”等)。
此外,我们还可以通过添加方法来实现一些计算逻辑,为 enum 添加计算值。
7. 控制接口设计
由于我们为 enum 添加了字段和方法,它的公共接口也发生了变化。而使用核心 Enum 的 name() 和 valueOf() 方法的代码,不会感知到我们新增的字段。
Java 语言已经为我们定义了 static 的 valueOf() 方法,因此我们无法自定义自己的 valueOf() 实现。
同样地,因为 Enum.name() 方法是 final 的,我们也不能覆盖它。
因此,我们无法通过标准的 Enum API 来使用我们新增的字段。下面来看几种暴露字段的方式。
7.1. 覆盖 toString()
覆盖 toString() 可以作为替代 name() 的方式:
@Override
public String toString() {
return this.label;
}
默认情况下,Enum.toString() 返回的值与 Enum.name() 相同。
7.2. 实现接口
✅ Java 中的 enum 类型可以实现接口。 虽然这种方式不如 Enum API 通用,但接口可以帮助我们抽象出通用行为。
考虑以下接口:
public interface Labeled {
String label();
}
为了与 Enum.name() 方法保持一致,我们的 label() 方法没有使用 get 前缀。
由于 valueOfLabel() 是 static 方法,我们不在接口中定义它。
最后,我们在 enum 中实现该接口:
public enum Element implements Labeled {
// ...
@Override
public String label() {
return label;
}
// ...
}
这种做法的好处是,Labeled 接口可以应用到任何类,而不仅限于 enum 类型。相比依赖通用的 Enum API,我们现在有了更具上下文语义的接口。
8. 总结
本文深入探讨了 Java Enum 的多种特性。通过添加构造函数、字段和方法,我们发现 enum 的能力远不止于简单的字面常量。
✅ enum 不仅能封装常量,还能携带额外数据、提供查找逻辑、甚至实现接口,是一个功能强大的类型。
如需获取完整源码,请访问 GitHub。