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).
DbIdEnumOps
within the private constructor, how can I validate that the enumClassE
passed also conforms to Class<EE>
?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 enum
s 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
.
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
}