javaenumsinterface

Getting class from interface static method


I have following code:

public interface IEnum {

    static <E extends Enum<E> & IEnum> Map<Integer, E> getId2EnumMap(Class<E> clazz) {
        Map<Integer, E> temp = new HashMap<>();
        for (final E e : clazz.getEnumConstants()) {
            temp.put(e.getId(), e);
        }
        return Collections.unmodifiableMap(temp);
    }

    int getId();
}
public enum TestEnum2 implements IEnum {
    ONE(1),
    TWOOOOOOO(22);

    public final Integer id;

    TestEnum2(int id) {
        this.id = id;
    }

    static final Map<Integer, TestEnum2> enumToId;
    static {
        enumToId = IEnum.getId2EnumMap(TestEnum2.class);
    }

    @Override
    public int getId() {
        return id;
    }
}

In this code, all I want to do is not have to pass TestEnum2.class to IEnum.getId2EnumMap. Is it possible either by modification of interface signature or some other way?

I have tried multiple threads from Stack Overflow, but every single one of them result in either of two things:

  1. Interface Function cannot be static which results in not being able to call it in TestEnum2 in static block

  2. It can be static but since method is interface method is static, I cannot get concrete type


Solution

  • During compiling all generics are erased. That means that the resulting signature is static Map getId2EnumMap(Class clazz). If the signature before erasure would be static <E extends Enum<E> & IEnum> Map<Integer, E> getId2EnumMap(), during runtime there would be nothing left to identify the type (note that C# which doesn't use erasure and does allow you to read the generic type during runtime). In other words, you need something to be able to get the constants. I see two options:

    1. Do what you do now.
    2. Replace Class<E> clazz with E[] constants and getEnumConstants() with constants. The call would become IEnum.getId2EnumMap(values()).

    Note that you can make the method a bit easier to read (at least, that's what I think):

    static <E extends Enum<E> & IEnum> Map<Integer, E> getId2EnumMap(Class<E> clazz) {
        return Arrays.stream(clazz.getEnumConstants())
                .collect(Collectors.toUnmodifiableMap(IEnum::getId, Function.identity()));
    }
    

    One final remark: you can get rid of the static initializer block by assigning the value directly:

    static final Map<Integer, TestEnum2> enumToId = IEnum.getId2EnumMap(TestEnum2.class);
    

    Edit: there is one way to base this on the current class, using StackWalker:

    public interface IEnum {
    
        static <E extends Enum<E> & IEnum> Map<Integer, E> getId2EnumMap() {
            StackWalker stackWalker = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE);
            Class<E> clazz = (Class<E>) stackWalker.getCallerClass();
            Map<Integer, E> temp = new HashMap<>();
            for (final E e : clazz.getEnumConstants()) {
                temp.put(e.getId(), e);
            }
            return Collections.unmodifiableMap(temp);
        }
    
        int getId();
    }
    
    public enum TestEnum2 implements IEnum {
        ONE(1),
        TWOOOOOOO(22);
    
        public final Integer id;
    
        TestEnum2(int id) {
            this.id = id;
        }
    
        static final Map<Integer, TestEnum2> enumToId = IEnum.getId2EnumMap();
    
        @Override
        public int getId() {
            return id;
        }
    }
    

    Note however that the cast is necessary to let clazz.getEnumConstants() return E[] but it gives an unchecked cast warning. The compiler won't prevent me from doing this in a completely unrelated class:

    static final Map<Integer, TestEnum2> enumToId = IEnum.getId2EnumMap();
    

    This will compile but give a ClassCastException when you try to get values from the map. You could add some checks in getId2EnumMap to at least make sure that IEnum.class.isAssignableFrom(clazz), but I can still call the above in a different class that implements IEnum. The issue is that you cannot verify that the generic type E matches the caller class in any way in Java - because the generic type is simply gone during runtime.