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_ENCODE
和 BASE64_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.Enum
的 final
类
例如,使用 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
类
❌ 即使能继承,也会出现多重继承问题(继承 BasicStringOperation
和 java.lang.Enum
)
3. 使用接口模拟可扩展枚举
虽然不能继承枚举,但接口是支持扩展的。因此,可以通过实现接口来模拟可扩展的枚举。
3.1. 模拟扩展枚举常量
以扩展 BasicStringOperation
添加 MD5_ENCODE
和 BASE64_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