The Joy Of Generics In Java

The Joy Of Generics In Java

Problem

Java enums are a powerful way to define a fixed set of constant values. They are commonly used to represent a finite set of options, such as status codes, types, or categories. However, I found myself repeating similar code blocks in multiple enum classes.

It's better to see what I mean by code example. Here is OriginType enum:

@Getter
public enum OriginType {

    DOMESTIC(1, "국내산"),
    FOREIGN(2, "외국산"),
    OTHER(3, "기타");

    private final int code;
    private final String description;

    private static final Map<Integer, OriginType> codeToEnum =
            Stream.of(values()).collect(toMap(e -> e.code, e -> e));

    OriginType(int code, String description) {
        this.code = code;
        this.description = description;
    }

    public static Optional<OriginType> fromCode(int code) {
        return Optional.ofNullable(codeToEnum.get(code));
    }

}

Nothing fancy here. It has only two public and one private field. The public method is self-explanatory, it gets an integer code and then returns the corresponding enum value. codeToEnum is just a cache of values to be used for the retrieval of enum values so that we don't have to iterate each time we need to covert code to an enum value. Then I have another enum class Status:

@Getter
public enum Status {

    ACTIVE(1, "진행중"),
    INACTIVE(2, "완료");

    private final int code;
    private final String description;

    private static final Map<Integer, Status> codeToEnum =
            Stream.of(values()).collect(toMap(e -> e.code, e -> e));

    Status(int code, String description) {
        this.code = code;
        this.description = description;
    }

    public static Optional<Status> fromCode(int code) {
        return Optional.ofNullable(codeToEnum.get(code));
    }

}

Again, nothing fancy the same as the previous one. However, the problem is with codeToEnum and fromCode(int code) methods, they are repeated. Even worse, I have dozens of enum classes like this and codeToEnum and fromCode(int code) are repeated in all of them. The only difference is the type, such as, OriginType or Status. Hmm, there should be a better way. At this point, I started playing with Generics. Let's see how generics can help with our particular problem. We will gradually improve our code to make the best use of generics at the end.

Initial Attempt

The first step is to create an interface for common enums since all of them have getCode() and getDescription() methods. We can keep fromCode(int code) method in the interface itself since it's a static method. This is how our interface looks like before we implement fromCode(int code) method:

public interface EnumType {

    int getCode();

    String getDescription();

    static Optional<EnumType> fromCode(int code) {
        // Fancy logic here
    }

}

However, we can't just magically figure out the subclass. To be able to retrieve all the values and check if the code can be converted to EnumType, we need to know the subtype that implements EnumType interface. The first idea that would come to any intermediate Java developer would be accepting Class as an argument then process the rest of the logic. Not surprisingly, I did the same and changed the method signature:

static Optional<EnumType> fromCode(Class<?> enumType, int code) {
        // Fancy logic here
}

The interface definition is ready, now implementation time. Here is the first attempt:

public interface EnumType {

    int getCode();

    String getDescription();

    static Optional<EnumType> fromCode(Class<?> enumType, int code) {
       // We deal with only enum values 
       if(!enumType.isEnum()) {
           return Optional.empty();
       }

       Object[] enumConstants = enumType.getEnumConstants();
       if(enumConstants.length == 0 || !(enumConstants[0] instanceof EnumType)) {
           return Optional.empty();
        }
        return Arrays.stream(enumConstants)
                .map(enumConstant -> (EnumType) enumConstant)
                .filter(enumConstant -> enumConstant.getCode() == code)
                .findFirst();
    }

}

We are doing a bunch of checks like if enumType is an enum and check if it has any enum constants. In this line !(enumConstants[0] instanceof EnumType) we are checking if the given subtype is the type of EnumType since we deal only with EnumTypes. The rest is self-explanatory, code to EnumType conversion logic.

Let's write some test code.

class EnumTypeTests {

    @Test
    void whenConvertToEnumFromCodeThenReturnOptional() {
        Optional<Status> status = EnumType.fromCode(Status.class, 1);
        assertThat(status).isNotEmpty().hasValue(Status.ONGOING);

        status = EnumType.fromCode(Status.class, 2);
        assertThat(status).isNotEmpty().hasValue(Status.FINISHED);

        status = EnumType.fromCode(Status.class, 55);
        assertThat(status).isEmpty();

        status = EnumType.fromCode(ArrayList.class, 12);
        assertThat(status).isEmpty();

        status = EnumType.fromCode(Stack.class, 12);
        assertThat(status).isEmpty();
    }

    private enum Status implements EnumType {

        ONGOING(1, "진행중"),
        FINISHED(2, "완료");

        private final int code;
        private final String description;

        Status(int code, String description) {
            this.code = code;
            this.description = description;
        }

        @Override
        public int getCode() {
            return this.code;
        }

        @Override
        public String getDescription() {
            return this.description;
        }

    }

}

Tests pass, checks are green and dopamine is released. We are happy.

The above logic works but we are losing the Java's type safety here.

    status = EnumType.fromCode(ArrayList.class, 12);
    assertThat(status).isEmpty();

    status = EnumType.fromCode(Stack.class, 12);
    assertThat(status).isEmpty();

Our code just accepts everything even if the type is not enum and doesn't implement EnumType. To deal with it, we are just writing a lot of if statements instead. The code is not elegant. There must be a better way. Hey, come on we are Java developers, not JavaScript, yes there must be a better way.

The Second Attempt

If we change fromCode() signature to accept only EnumType we can get rid of this line.

if(enumConstants.length == 0 || !(enumConstants[0] instanceof EnumType)) {
    return Optional.empty();
}

To achieve this, we introduce parameterized type to our method signature:

static Optional<EnumType> fromCode(Class<? extends EnumType> enumType, int code) {
    if(!enumType.isEnum()) {
        return Optional.empty();
    }
       EnumType[] enumConstants = enumType.getEnumConstants();
    return Arrays.stream(enumConstants)
            .filter(enumConstant -> enumConstant.getCode() == code)
            .findFirst();
}

The Class<? extends EnumType> is a wildcard type that allows any subclass of EnumType is to be used as a type argument.

With this simple change EnumType.fromCode(ArrayList.class, 12) or EnumType.fromCode(Stack.class, 12) doesn't compile, we are type safe.

However, it's not fully type safe yet. Any subclass type that implements EnumType can be passed to fromCode() method even if it is not an enum. EnumType.fromCode(NotEnum.class, 12) still compiles with the below implementation class.

public class NotEnum implements EnumType {

    @Override
    public int getCode() {
        return this.code;
    }

    @Override
    public String getDescription() {
        return this.description;
    }

}

There still must be room for improvement.

The Third Attempt

At this point, we need to define a type that is both enum and implements EnumType class. Defining a such type would allow us to remove those extra if statements and reach a full type safety of Java. As a result, be closer to a real Java(not JavaScript) developer.

Fortunately, there is a way. In technical(fancy) terms, it's called intersection type. It simply means a type that represents a combination of multiple types. It allows you to create a new type that includes the common features of the types being intersected.

Let's see how we can use it in our problem:

static <E extends Enum<E> & EnumType> Optional<E> fromCode(Class<E> enumType, int code) {
    return Arrays.stream(enumType.getEnumConstants())
            .filter(a -> a.getCode() == code)
            .findFirst();
}

Now, let's decompose our logic. We defined a type of constrained E using two conditions:

  1. E extends Enum<E>: This means that E must be an enumeration type, meaning it should be an enum class or a subclass of an enum class.

  2. E extends EnumType: This means that E must also implement the EnumType interface.

After the change, this code doesn't compile: EnumType.fromCode(NotEnum.class, 12). Therefore, we don't even need this line(type safety).

if(!enumType.isEnum()) {
    return Optional.empty();
}

Here is the full revised code that is type safe and elegant(Java).

public interface EnumType {

    int getCode();

    String getDescription();

    static <E extends Enum<E> & EnumType> Optional<E> fromCode(Class<E> enumType, int code) {
        return Arrays.stream(enumType.getEnumConstants())
                .filter(a -> a.getCode() == code)
                .findFirst();
    }

}

We also need to update our test class since some lines don't compile.

class EnumTypeTests {

    @Test
    void whenConvertToEnumFromCodeThenReturnOptional() {
        Optional<Status> status = EnumType.fromCode(Status.class, 1);
        assertThat(status).isNotEmpty().hasValue(Status.ONGOING);

        status = EnumType.fromCode(Status.class, 2);
        assertThat(status).isNotEmpty().hasValue(Status.FINISHED);

        status = EnumType.fromCode(Status.class, 55);
        assertThat(status).isEmpty();
//      Doesn't compile
//      status = EnumType.fromCode(ArrayList.class, 12);
//      assertThat(status).isEmpty();

//      Doesn't compile
//      status = EnumType.fromCode(Stack.class, 12);
//      assertThat(status).isEmpty();
    }

    private enum Status implements EnumType {

        ONGOING(1, "진행중"),
        FINISHED(2, "완료");

        private final int code;
        private final String description;

        Status(int code, String description) {
            this.code = code;
            this.description = description;
        }

        @Override
        public int getCode() {
            return this.code;
        }

        @Override
        public String getDescription() {
            return this.description;
        }

    }

}

As you can see, we got rid of our repetitive code. We don't have to create fromCode() method in every enum class.

Extra Attempt

Tests pass, checks are green and dopamine is released. We are happy.

However, there is still a little bit of room for optimization. Do you remember the caching logic from the beginning of the article? Sample from Status enum:

private static final Map<Integer, Status> codeToEnum =
     Stream.of(values()).collect(toMap(e -> e.code, e -> e));

We are not using any caching logic in our EnumType class like the one above. Instead, we are iterating over enum values every time. The reader might say, that's a premature optimization you don't necessarily need because you won't have too many enum values. That may be true, but hey, let's push the boundaries.

Time to write some caching logic. It would be better to put the caching logic into a separate class instead of cluttering our interface with too much code. Also with this approach, we deal only with abstraction, leaving the implementation to someone else. Let's call this someone else EnumValueResolver and assume it will have the same method signature. We need to change our EnumType interface accordingly.

public interface EnumType {

    int getCode();

    String getDescription();

    static <E extends Enum<E> & EnumType> Optional<E> fromCode(Class<E> enumType, int code) {
        return EnumValueResolver.fromCode(enumType, code);
    }

}

Next, implement EnumValueResolver caching logic.

/**
 * For internal use only.
 * <p>
 * Use the {@link EnumType#fromCode(Class, int)} instead
 * </p>
 */
final class EnumValueResolver {

    private static final Map<Class<?>, Map<Integer, ? extends EnumType>> CACHE = new ConcurrentHashMap<>();

    private EnumValueResolver() {}

    @SuppressWarnings("unchecked")
    static <E extends Enum<E> & EnumType> Optional<E> fromCode(Class<E> enumType, int code) {
        E value = (E) CACHE.computeIfAbsent(enumType, key -> stream(enumType.getEnumConstants())
                        .collect(toMap(E::getCode, identity()))).get(code);
        return Optional.ofNullable(value);
    }

}

The code might be a little bit hard to understand at first glance. It's similar to previous caching logic but this time we are mapping the subtype to a map of integer code to a EnumType like we did in codeToEnum. The rest is just computing and retrieving logic.

Conclusion

We learned how to make the best use of generics to avoid repetitive code, reach full type safety and write more elegant(Java) code.