1. Overview
The enum type, introduced in Java 5, is a special data type that represents a group of constants.
Using enums, we can define and use our constants in the way of type safety. It brings compile-time checking to the constants. Furthermore, it allows us to use the constants in the switch-case statement.
In this tutorial, we’ll discuss extending enums in Java, including adding new constant values and new functionalities.
2. Enums and Inheritance
When we want to extend a Java class, we typically create a subclass. In Java, enums are classes as well.
In this section, we’ll see if we can inherit an enum, as we do with regular Java classes.
2.1. Extending an Enum Type
First, let’s look at an example, so we can quickly understand the problem:
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.
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:
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
Let’s figure out why we received a compiler error.
When we compile an enum, the Java compiler does some magic to it:
- 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’s represented as a subclass of java.lang.Enum
$ 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 isn’t supported in Java.
3. Emulate Extensible Enums With Interfaces
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
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.
First, we’ll create an interface*,* StringOperation:
public interface StringOperation {
String getDescription();
}
Next, we’ll 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, we’ll see how to emulate an extensible BasicStringOperation enum.
Let’s say we have a method in our application to get the description of the BasicStringOperation enum:
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:
public String getOperationDescription(StringOperation stringOperation) {
return stringOperation.getDescription();
}
3.2. Extending Functionalities
We’ve demonstrated how to emulate extending constants of enums with interfaces. We can also add methods to the interface to extend the functionalities of the enums.
For example, let’s say we want to extend our StringOperation enums so that each constant can actually apply the operation to a given string:
public class Application {
public String applyOperation(StringOperation operation, String input) {
return operation.apply(input);
}
//...
}
To achieve this, we’ll add the apply() method to the interface:
public interface StringOperation {
String getDescription();
String apply(String input);
}
Next, we’ll let each StringOperation enum implement this method:
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 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
We’ve learned how to extend an enum by implementing interfaces, but sometimes, we want to extend the functionalities of an enum without modifying it. For example, we might want to extend an enum from a third-party library.
4.1. Associating Enum Constants and Interface Implementations
First, we’ll look at an enum example:
public enum ImmutableOperation {
REMOVE_WHITESPACES, TO_LOWER, INVERT_CASE
}
Let’s say the enum is from an external library, and 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:
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.
First, we’ll create an interface:
public interface Operator {
String apply(String input);
}
Then we’ll create the mapping between enum constants and the Operator implementations using an EnumMap<ImmutableOperation, 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:
@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
If the enum is from an external library, we won’t know if it’s been changed or not, such as by adding new constants to the enum. As a result, 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.
To avoid this, we can validate the EnumMap after its initialization to check if it contains all enum constants:
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 isn’t mapped, an IllegalStateException will be thrown. Since our validation is in a static block, IllegalStateException will be the cause of ExceptionInInitializerError:
@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.
5. Conclusion
The enum is a special data type in Java. In this article, we discussed why enums don’t support inheritance. Then we addressed how to emulate extensible enums with interfaces. We also 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.