1. 概述

Java 5 引入的 enum 是一种特殊的数据类型,用于表示一组常量。

使用枚举可以实现类型安全的常量定义,并在编译期进行校验。同时,枚举还支持在 switch-case 语句中使用。

本文将探讨如何在 Java 中扩展枚举,包括新增常量值和功能。

2. 枚举与继承

当我们想要扩展一个类时,通常会创建子类。在 Java 中,枚举本质上也是类。

本节我们将探讨是否可以像普通类一样继承枚举。

2.1. 尝试扩展枚举类型

先来看一个例子,快速理解问题所在:

public enum BasicStringOperation {
    TRIM("Removing leading and trailing spaces."),
    TO_UPPER("Changing all characters into upper case."),
    REVERSE("Reversing the given string.");

    private String description;

    // constructor and getter
}

如上所示,我们有一个名为 BasicStringOperation 的枚举,包含三个基本字符串操作。

假设我们要扩展这个枚举,添加如 MD5_ENCODEBASE64_ENCODE 这样的操作。我们可能会想到如下写法:

public enum ExtendedStringOperation extends BasicStringOperation {
    MD5_ENCODE("Encoding the given string using the MD5 algorithm."),
    BASE64_ENCODE("Encoding the given string using the BASE64 algorithm.");

    private String description;

    // constructor and getter
}

然而,编译时会报错:

Cannot inherit from enum BasicStringOperation

2.2. 枚举不支持继承的原因

为什么不能继承枚举?原因如下:

Java 编译器会将枚举编译为继承自 java.lang.Enumfinal

例如,使用 javap 反编译 BasicStringOperation

$ javap BasicStringOperation  
public final class com.baeldung.enums.extendenum.BasicStringOperation 
    extends java.lang.Enum<com.baeldung.enums.extendenum.BasicStringOperation> {
  public static final com.baeldung.enums.extendenum.BasicStringOperation TRIM;
  public static final com.baeldung.enums.extendenum.BasicStringOperation TO_UPPER;
  public static final com.baeldung.enums.extendenum.BasicStringOperation REVERSE;
 ...
}

不能继承 final

即使能继承,也会出现多重继承问题(继承 BasicStringOperationjava.lang.Enum

3. 使用接口模拟可扩展枚举

虽然不能继承枚举,但接口是支持扩展的。因此,可以通过实现接口来模拟可扩展的枚举

3.1. 模拟扩展枚举常量

以扩展 BasicStringOperation 添加 MD5_ENCODEBASE64_ENCODE 为例。

首先定义接口:

public interface StringOperation {
    String getDescription();
}

然后让两个枚举实现该接口:

public enum BasicStringOperation implements StringOperation {
    TRIM("Removing leading and trailing spaces."),
    TO_UPPER("Changing all characters into upper case."),
    REVERSE("Reversing the given string.");

    private String description;
    // constructor and getter override
}

public enum ExtendedStringOperation implements StringOperation {
    MD5_ENCODE("Encoding the given string using the MD5 algorithm."),
    BASE64_ENCODE("Encoding the given string using the BASE64 algorithm.");

    private String description;

    // constructor and getter override
}

接着修改方法参数类型为接口:

public String getOperationDescription(StringOperation stringOperation) {
    return stringOperation.getDescription();
}

这样就可以统一处理两个枚举中的常量。

3.2. 扩展功能

接口不仅可以扩展常量,还可以扩展方法。

比如我们希望每个操作都能真正作用于字符串:

public class Application {
    public String applyOperation(StringOperation operation, String input) {
        return operation.apply(input);
    }
    //...
}

为此,在接口中添加方法:

public interface StringOperation {
    String getDescription();
    String apply(String input);
}

然后在每个枚举中实现该方法:

public enum BasicStringOperation implements StringOperation {
    TRIM("Removing leading and trailing spaces.") {
        @Override
        public String apply(String input) { 
            return input.trim(); 
        }
    },
    TO_UPPER("Changing all characters into upper case.") {
        @Override
        public String apply(String input) {
            return input.toUpperCase();
        }
    },
    REVERSE("Reversing the given string.") {
        @Override
        public String apply(String input) {
            return new StringBuilder(input).reverse().toString();
        }
    };

    //...
}

public enum ExtendedStringOperation implements StringOperation {
    MD5_ENCODE("Encoding the given string using the MD5 algorithm.") {
        @Override
        public String apply(String input) {
            return DigestUtils.md5Hex(input);
        }
    },
    BASE64_ENCODE("Encoding the given string using the BASE64 algorithm.") {
        @Override
        public String apply(String input) {
            return new String(new Base64().encode(input.getBytes()));
        }
    };

    //...
}

测试代码如下:

@Test
public void givenAStringAndOperation_whenApplyOperation_thenGetExpectedResult() {
    String input = " hello";
    String expectedToUpper = " HELLO";
    String expectedReverse = "olleh ";
    String expectedTrim = "hello";
    String expectedBase64 = "IGhlbGxv";
    String expectedMd5 = "292a5af68d31c10e31ad449bd8f51263";
    assertEquals(expectedTrim, app.applyOperation(BasicStringOperation.TRIM, input));
    assertEquals(expectedToUpper, app.applyOperation(BasicStringOperation.TO_UPPER, input));
    assertEquals(expectedReverse, app.applyOperation(BasicStringOperation.REVERSE, input));
    assertEquals(expectedBase64, app.applyOperation(ExtendedStringOperation.BASE64_ENCODE, input));
    assertEquals(expectedMd5, app.applyOperation(ExtendedStringOperation.MD5_ENCODE, input));
}

4. 无需修改代码扩展枚举

有时候我们希望扩展一个第三方库中的枚举,而不能修改其源码。

4.1. 关联枚举常量与实现

假设有一个第三方枚举:

public enum ImmutableOperation {
    REMOVE_WHITESPACES, TO_LOWER, INVERT_CASE
}

我们希望在应用中为这些操作提供实现。

可以使用 EnumMap 将枚举常量与其操作实现进行映射:

public class Application {
    private static final Map<ImmutableOperation, Operator> OPERATION_MAP;

    static {
        OPERATION_MAP = new EnumMap<>(ImmutableOperation.class);
        OPERATION_MAP.put(ImmutableOperation.TO_LOWER, String::toLowerCase);
        OPERATION_MAP.put(ImmutableOperation.INVERT_CASE, StringUtils::swapCase);
        OPERATION_MAP.put(ImmutableOperation.REMOVE_WHITESPACES, input -> input.replaceAll("\\s", ""));
    }

    public String applyImmutableOperation(ImmutableOperation operation, String input) {
        return OPERATION_MAP.get(operation).apply(input);
    }
}

接口定义如下:

public interface Operator {
    String apply(String input);
}

测试代码:

@Test
public void givenAStringAndImmutableOperation_whenApplyOperation_thenGetExpectedResult() {
    String input = " He ll O ";
    String expectedToLower = " he ll o ";
    String expectedRmWhitespace = "HellO";
    String expectedInvertCase = " hE LL o ";
    assertEquals(expectedToLower, app.applyImmutableOperation(ImmutableOperation.TO_LOWER, input));
    assertEquals(expectedRmWhitespace, app.applyImmutableOperation(ImmutableOperation.REMOVE_WHITESPACES, input));
    assertEquals(expectedInvertCase, app.applyImmutableOperation(ImmutableOperation.INVERT_CASE, input));
}

4.2. 校验 EnumMap 的完整性

如果第三方枚举后续新增了常量,而我们的 EnumMap 没有同步更新,可能会导致运行时错误。

为避免这种情况,可以在初始化后校验 EnumMap 是否包含了所有枚举常量:

static {
    OPERATION_MAP = new EnumMap<>(ImmutableOperation.class);
    OPERATION_MAP.put(ImmutableOperation.TO_LOWER, String::toLowerCase);
    OPERATION_MAP.put(ImmutableOperation.INVERT_CASE, StringUtils::swapCase);
    // ImmutableOperation.REMOVE_WHITESPACES is not mapped

    if (Arrays.stream(ImmutableOperation.values()).anyMatch(it -> !OPERATION_MAP.containsKey(it))) {
        throw new IllegalStateException("Unmapped enum constant found!");
    }
}

如果发现未映射的常量,抛出异常:

@Test
public void givenUnmappedImmutableOperationValue_whenAppStarts_thenGetException() {
    Throwable throwable = assertThrows(ExceptionInInitializerError.class, () -> {
        ApplicationWithEx appEx = new ApplicationWithEx();
    });
    assertTrue(throwable.getCause() instanceof IllegalStateException);
}

这样,一旦遗漏了某个枚举常量,应用启动时就会报错,避免运行时出错。

5. 总结

枚举是 Java 中的特殊类型,由于其编译后的 final 特性,无法通过继承来扩展。

✅ 推荐做法是使用接口来模拟可扩展枚举
✅ 可通过 EnumMap 为不可变枚举扩展功能
✅ 一定要校验映射完整性,避免遗漏常量

源码地址:GitHub


原始标题:Extending Enums in Java