1. 概述

Java 的 enum 类型为创建和使用常量提供了一种语言级别的支持。相比 Stringint 这类字面量常量,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;
}

这个 staticvalueOfLabel() 方法会遍历所有 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 添加了字段和方法,它的公共接口也发生了变化。而使用核心 Enumname()valueOf() 方法的代码,不会感知到我们新增的字段。

Java 语言已经为我们定义了 staticvalueOf() 方法,因此我们无法自定义自己的 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


原始标题:Attaching Values to Java Enum