Quantcast
Channel: Baeldung
Viewing all articles
Browse latest Browse all 4535

Extending Enums in Java

$
0
0

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.

Further, it allows us to use the constants in the switch-case statement.

In this tutorial, we'll discuss extending enums in Java, for instance, adding new constant values and new functionalities.

2. Enums and Inheritance

When we want to extend a Java class, we'll typically create a subclass. In Java, enums are classes as well.

In this section, let's see if we can inherit an enum as we do with regular Java classes.

2.1. Extending an Enum Type

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.

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

Now, let's find out why we got our 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 is represented as a subclass of java.lang.Enum<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;
 ...
}

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.

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, let's create an interface 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.

Let's say we have a method in our application to get the description of 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 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:

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:

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

Next, we 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 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

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

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:

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, 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>:

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

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.

To avoid that, 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 is not 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've discussed why enum doesn't support inheritance. After that, we addressed how to emulate extensible enums with interfaces.

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.

The post Extending Enums in Java first appeared on Baeldung.

        

Viewing all articles
Browse latest Browse all 4535

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>