Search code examples
javagenericsenumsinterface

How Do I Validate At Compile Time a Java Enum Conforms to Two Different Types


UPDATE on 2023-10-26: Here's a gist of the example source code, which has been refactored to incorporate the solution provided within the answer provided by @Turing85.


Given the code listed below across five classes, I have two questions regarding resolving the types at compile time (explicitly NOT at run time).

  1. In DbIdEnumOps within the private constructor, how can I validate that the enumClassE passed also conforms to Class<EE>?
  2. In Main within the main method, how do I get a to infer where it shows as Integer, instead of Object, without resorting to b where I provide an explicit type hint?

I have spent hours trying different tangents to get this to work. And the code below is as close as I've been able to get.


Class: EnumOps.java

public final class EnumOps<E extends Enum<E>> {
  private final Class<E> enumClass;
  private final List<E> enumsValues;

  private EnumOps(Class<E> enumClass) {
    this.enumClass = enumClass;
    this.enumsValues = Collections.unmodifiableList(Arrays.asList(enumClass.getEnumConstants()));
  }

  public static <E extends Enum<E>> EnumOps<E> from(Class<E> enumClass) {
    return new EnumOps<>(enumClass);
  }

  public Class<E> getEnumClass() {
    return this.enumClass;
  }

  public List<E> toList() {
    return this.enumsValues;
  }

  //omitted lots of other helpful enum utilities that ought to have been provided by the Java compiler by default
}

Interface: DbIdEnum.java

public interface DbIdEnum<T> {
  T getDbId();
}

Class: DbIdEnumOps.java

public final class DbIdEnumOps<E extends Enum<E>, EE extends DbIdEnum<T>, T> {
  private final Map<T, E> enumValueByDbId;
  private final Map<String, E> enumValueByNameLowerCaseOrDbId;

  private DbIdEnum<T> toDbIdEnum(E e) {
    return ((DbIdEnum<T>) e);
  }

  public static <E extends Enum<E>, EE extends DbIdEnum<T>, T> DbIdEnumOps<E, EE, T> from(Class<E> enumClassE) {
    return new DbIdEnumOps<>(enumClassE);
  }

  private DbIdEnumOps(Class<E> enumClassE) {
    //how to validate AT COMPILE TIME `enumClassE` also conforms to `Class<EE>`?
    var enumOpsList =
        EnumOps.from(Objects.requireNonNull(enumClassE))
            .toList();
    this.enumValueByDbId =
        Collections.unmodifiableMap(
            enumOpsList
                .stream()
                .map(e -> Map.entry(toDbIdEnum(e).getDbId(), e))
                .collect(Collectors.toMap(Entry::getKey, Entry::getValue)));
    this.enumValueByNameLowerCaseOrDbId =
        Collections.unmodifiableMap(
            enumOpsList
                .stream()
                .flatMap(e -> Stream.of(
                    Map.entry(e.name().toLowerCase(), e),
                    Map.entry(toDbIdEnum(e).getDbId().toString(), e)))
                .collect(Collectors.toMap(Entry::getKey, Entry::getValue)));
  }

  public Map<T, E> getEnumValueByDbId() {
    return this.enumValueByDbId;
  }

  public Map<String, E> getEnumValueByNameLowerCaseOrDbId() {
    return this.enumValueByNameLowerCaseOrDbId;
  }
}

Enum: TrafficLight.java

public enum TrafficLight implements DbIdEnum<Integer> {
  GREEN(1),
  YELLOW(2),
  RED(3);

  private final Integer dbId;

  TrafficLight(Integer dbId) {
    this.dbId = dbId;
  }

  private static final EnumOps<TrafficLight> enumOps =
      EnumOps.from(TrafficLight.class);
  private static final DbIdEnumOps<TrafficLight, DbIdEnum<Integer>, Integer> dbIdEnumOpsA =
      DbIdEnumOps.from(TrafficLight.class);

  public static EnumOps<TrafficLight> ops() {
    return enumOps;
  }

  public static DbIdEnumOps<TrafficLight, DbIdEnum<Integer>, Integer> dbIdOps() {
    return dbIdEnumOpsA;
  }

  public Integer getDbId() {
    return this.dbId;
  }
}

Class: Main.java

public class Main {

  public static void main(String[] args) {
    //a is inferred as: DbIdEnumOps<TrafficLight, DbIdEnum<Object>, Object>
    var a = DbIdEnumOps.from(TrafficLight.class);
    var b = DbIdEnumOps.<TrafficLight, DbIdEnum<Integer>, Integer>from(TrafficLight.class);
    DbIdEnumOps<TrafficLight, DbIdEnum<Integer>, Integer> c = DbIdEnumOps.from(TrafficLight.class);
    System.out.println("use me to set a breakpoint to examine the contents of the maps");

    //How do I get `a` to infer where it shows as `Integer`, instead of `Object`, without resorting to `b` where I provide an explicit type hint?
  }
}

For 1, a tangent I took involved passing in the same enum class twice to the from method where the method's signature looked like this:

  public static <E extends Enum<E>, EE extends DbIdEnum<T>, T> DbIdEnumOps<E, EE, T> from(Class<EE> enumClassEe, Class<E> enumClassE) {
    return new DbIdEnumOps<>(enumClassEe, enumClassE);
  }

  private DbIdEnumOps(Class<EE> enumClassEe, Class<E> enumClassE) {...

And while I got that to work, passing the enum in twice didn't look correct just to satisfy both types.

I figure there is some arcane or unusual type specification pathway that remains right outside my understanding. Any guidance with this would be deeply appreciated.


Background: For those more curious about the context, I am attempting to DRY (Don't Repeat Yourself) out a bunch of enums being used across multiple code bases. Many, but not ALL, of the enums are used for database columns. Hence, the explicit (and required) separation of concerns between EnumOps and DbIdEnumOps.


Solution

  • We can restrict the generic type to comply with multiple implementations through an &. If we want some E to be an Enum<E> and implement some interface Foo, we ca write E extends Enum<E> & Foo. Of course, Foo can also have a generic parameter, which we can either bind (e.g. E extends Enum<E> & Foo<Integer>) or introduce an additional generic parameter (I, E extends Enum<E> & Foo<I>). Putting it all together, we get:

    class Ideone {
      public static void main(String[] args) {
        boo(Bar.bar1);
    //    foo(new Baz()); // should not work - does not work
    //    foo(Bang.bang1); // should not work - does not work
      }
    
      public static <T extends Enum<T> & Foo<Integer>> void boo(T t) {
        System.out.println(t.ordinal());
        System.out.println(t.foo());
      }
    }
    
    interface Foo<T> {
      T foo();
    }
    
    enum Bar implements Foo<Integer> {
      bar1(1);
    
      final int foo;
    
      Bar(int foo) {
        this.foo = foo;
      }
    
      @Override
      public Integer foo() {
        return foo;
      }
    }
    
    class Baz implements Foo<Integer> {
      @Override
      public Integer foo() {
        return 42;
      }
    }
    
    enum Bang {
      bang1
    }
    

    Ideone.com demo