1. Overview
1.概述
The enum type, introduced in Java 5, is a special data type that represents a group of constants.
enum类型,在Java 5中引入,是一种特殊的数据类型,代表一组常量。
Using enums, we can define and use our constants in the way of type safety. It brings compile-time checking to the constants.
使用枚举,我们可以以类型安全的方式定义和使用我们的常量。它给常量带来了编译时的检查。
Further, it allows us to use the constants in the switch-case statement.
此外,它允许我们在switch-case语句中使用常数。
In this tutorial, we’ll discuss extending enums in Java, for instance, adding new constant values and new functionalities.
在本教程中,我们将讨论在Java中扩展枚举,例如,增加新的常量值和新的功能。
2. Enums and Inheritance
2.枚举和继承
When we want to extend a Java class, we’ll typically create a subclass. In Java, enums are classes as well.
当我们想扩展一个Java类时,我们通常会创建一个子类。在Java中,枚举也是类。
In this section, let’s see if we can inherit an enum as we do with regular Java classes.
在本节中,让我们看看我们是否可以像对待普通的Java类那样继承一个枚举。
2.1. Extending an Enum Type
2.1.扩展一个枚举类型
First of all, let’s have a look at an example so that we can understand the problem quickly:
首先,让我们看看一个例子,以便我们能够快速理解这个问题。
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
}
As the code above shows, we have an enum BasicStringOperation that contains three basic string operations.
如上面的代码所示,我们有一个枚举BasicStringOperation,包含三种基本的字符串操作。
Now, let’s say we want to add some extension to the enum, such as MD5_ENCODE and BASE64_ENCODE. We may come up with this straightforward solution:
现在,假设我们想给枚举添加一些扩展,比如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
}
However, when we attempt to compile the class, we’ll see the compiler error:
然而,当我们试图编译该类时,我们会看到编译器错误。
Cannot inherit from enum BasicStringOperation
2.2. Inheritance Is Not Allowed for Enums
2.2.枚举不允许有继承性
Now, let’s find out why we got our compiler error.
现在,让我们来看看为什么我们得到了编译器错误。
When we compile an enum, the Java compiler does some magic to it:
当我们编译一个枚举时,Java编译器会对它施展一些魔法。
- It turns the enum into a subclass of the abstract class java.lang.Enum
- It compiles the enum as a final class
For example, if we disassemble our compiled BasicStringOperation enum using javap, we’ll see it is represented as a subclass of java.lang.Enum<BasicStringOperation>:
例如,如果我们使用javap拆解我们编译的BasicStringOperation枚举,我们会看到它被表示为java.lang.Enum<BasicStringOperation>/em>的子类。
$ 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;
...
}
As we know, we can’t inherit a final class in Java. Moreover, even if we could create the ExtendedStringOperation enum to inherit BasicStringOperation, our ExtendedStringOperation enum would extend two classes: BasicStringOperation and java.lang.Enum. That is to say, it would become a multiple inheritance situation, which is not supported in Java.
正如我们所知,在Java中我们不能继承一个final类。此外,即使我们可以创建ExtendedStringOperation枚举来继承BasicStringOperation,我们的ExtendedStringOperation枚举将扩展两个类。BasicStringOperation和java.lang.Enum。也就是说,这将成为一种多重继承的情况,这在Java中是不被支持的。
3. Emulate Extensible Enums With Interfaces
3.用接口模拟可扩展的枚举
We’ve learned that we can’t create a subclass of an existing enum. However, an interface is extensible. Therefore, we can emulate extensible enums by implementing an interface.
我们已经知道,我们不能创建一个现有枚举的子类。然而,一个接口是可扩展的。因此,我们可以通过实现一个接口来模拟可扩展的枚举。
3.1. Emulate Extending the Constants
3.1.模仿扩展常量
To understand this technique quickly, let’s have a look at how to emulate extending our BasicStringOperation enum to have MD5_ENCODE and BASE64_ENCODE operations.
为了快速理解这个技术,让我们看看如何模拟扩展我们的BasicStringOperation枚举,以拥有MD5_ENCODE和BASE64_ENCODE操作。
First, let’s create an interface StringOperation:
首先,让我们创建一个接口 StringOperation。
public interface StringOperation {
String getDescription();
}
Next, we make both enums implement the interface above:
接下来,我们使两个枚举都实现上述接口。
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
}
Finally, let’s have a look at how to emulate an extensible BasicStringOperation enum.
最后,让我们看看如何模拟一个可扩展的BasicStringOperation枚举。
Let’s say we have a method in our application to get the description of BasicStringOperation enum:
假设我们的应用程序中有一个方法来获取BasicStringOperation枚举的描述。
public class Application {
public String getOperationDescription(BasicStringOperation stringOperation) {
return stringOperation.getDescription();
}
}
Now we can change the parameter type BasicStringOperation into the interface type StringOperation to make the method accept instances from both enums:
现在我们可以将参数类型BasicStringOperation改为接口类型StringOperation,以使该方法接受两个枚举的实例。
public String getOperationDescription(StringOperation stringOperation) {
return stringOperation.getDescription();
}
3.2. Extending Functionalities
3.2.扩展功能
We’ve seen how to emulate extending constants of enums with interfaces.
我们已经看到了如何用接口来模拟扩展枚举的常量。
Further, we can also add methods to the interface to extend the functionalities of the enums.
此外,我们还可以向接口添加方法来扩展枚举的功能。
For example, we want to extend our StringOperation enums so that each constant can actually apply the operation to a given string:
例如,我们想扩展我们的StringOperation枚举,以便每个常量都能实际应用于给定字符串的操作。
public class Application {
public String applyOperation(StringOperation operation, String input) {
return operation.apply(input);
}
//...
}
To achieve that, first, let’s add the apply() method to the interface:
为了实现这一点,首先,让我们把apply()方法添加到接口。
public interface StringOperation {
String getDescription();
String apply(String input);
}
Next, we let each StringOperation enum implement this method:
接下来,我们让每个StringOperation枚举实现这个方法。
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()));
}
};
//...
}
A test method proves that this approach works as we expected:
一种测试方法证明了这种方法如我们所预期的那样工作。
@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. Extending an Enum Without Changing the Code
4.在不改变代码的情况下扩展一个枚举
We’ve learned how to extend an enum by implementing interfaces.
我们已经学会了如何通过实现接口来扩展一个枚举。
However, sometimes, we want to extend the functionalities of an enum without modifying it. For example, we’d like to extend an enum from a third-party library.
然而,有时候,我们想在不修改枚举的情况下扩展其功能。例如,我们想从一个第三方库中扩展一个枚举。
4.1. Associating Enum Constants and Interface Implementations
4.1.关联枚举常量和接口实现
First, let’s have a look at an enum example:
首先,让我们看一下一个枚举的例子。
public enum ImmutableOperation {
REMOVE_WHITESPACES, TO_LOWER, INVERT_CASE
}
Let’s say the enum is from an external library, therefore, we can’t change the code.
比方说,这个枚举是来自一个外部库,因此,我们不能改变代码。
Now, in our Application class, we want to have a method to apply the given operation to the input string:
现在,在我们的Application类中,我们希望有一个方法来对输入的字符串应用给定的操作。
public String applyImmutableOperation(ImmutableOperation operation, String input) {...}
Since we can’t change the enum code, we can use EnumMap to associate the enum constants and required implementations.
由于我们不能改变枚举的代码,我们可以使用EnumMap来关联枚举常量和所需的实现。
First, let’s create an interface:
首先,让我们创建一个接口。
public interface Operator {
String apply(String input);
}
Next, we’ll create the mapping between enum constants and the Operator implementations using an EnumMap<ImmutableOperation, Operator>:
接下来,我们将使用EnumMap<ImmutableOperation, Operator>创建枚举常量和Operator实现之间的映射。
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 operationMap.get(operation).apply(input);
}
In this way, our applyImmutableOperation() method can apply the corresponding operation to the given input string:
这样,我们的applyImmutableOperation()方法可以对给定的输入字符串应用相应的操作。
@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. Validating the EnumMap Object
4.2.验证EnumMap对象
Now, if the enum is from an external library, we don’t know if it has been changed or not, such as by adding new constants to the enum. In this case, if we don’t change our initialization of the EnumMap to contain the new enum value, our EnumMap approach may run into a problem if the newly added enum constant is passed to our application.
现在,如果这个枚举是来自外部库,我们不知道它是否被改变了,比如给枚举添加新的常量。在这种情况下,如果我们不改变对EnumMap的初始化以包含新的枚举值,如果新添加的枚举常量被传递给我们的应用程序,我们的EnumMap方法可能会遇到问题。
To avoid that, we can validate the EnumMap after its initialization to check if it contains all enum constants:
为了避免这种情况,我们可以在初始化后验证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!");
}
}
As the code above shows, if any constant from ImmutableOperation is not mapped, an IllegalStateException will be thrown. Since our validation is in a static block, IllegalStateException will be the cause of ExceptionInInitializerError:
正如上面的代码所示,如果ImmutableOperation中的任何常量没有被映射,将抛出IllegalStateException。由于我们的验证是在一个静态块中,IllegalStateException将成为ExceptionInitializerError的原因。
@Test
public void givenUnmappedImmutableOperationValue_whenAppStarts_thenGetException() {
Throwable throwable = assertThrows(ExceptionInInitializerError.class, () -> {
ApplicationWithEx appEx = new ApplicationWithEx();
});
assertTrue(throwable.getCause() instanceof IllegalStateException);
}
Thus, once the application fails to start with the mentioned error and cause, we should double-check the ImmutableOperation to make sure all constants are mapped.
因此,一旦应用程序启动失败,出现上述错误和原因,我们应该仔细检查ImmutableOperation,以确保所有常量都被映射了。
5. Conclusion
5.总结
The enum is a special data type in Java. In this article, we’ve discussed why enum doesn’t support inheritance. After that, we addressed how to emulate extensible enums with interfaces.
枚举是Java中一种特殊的数据类型。在这篇文章中,我们已经讨论了为什么枚举不支持继承。之后,我们讨论了如何用接口来模拟可扩展的枚举。
Also, we’ve learned how to extend the functionalities of an enum without changing it.
此外,我们还学会了如何在不改变枚举的情况下扩展它的功能。
As always, the full source code of the article is available over on GitHub.
一如既往,该文章的完整源代码可在GitHub上获得。