8

It seems I'm stuck with java generics again. Here is what I have:

Couple of classes:

class CoolIndex implements EntityIndex<CoolEntity>

class CoolEntity extends BaseEntity

Enum using classes above:

enum Entities {
    COOL_ENTITY {
        @Override
        public <E extends BaseEntity, I extends EntityIndex<E>> Class<I> getIndexCls() {
            return CoolIndex.class;
        }

        @Override
        public <E extends BaseEntity> Class<E> getEntityCls() {
            return CoolEntity.class;
        }
    }

    public abstract <E extends BaseEntity, I extends EntityIndex<E>> Class<I> getIndexCls();

    public abstract <E extends BaseEntity> Class<E> getEntityCls();    
}

Function I need to call with use of result of getIndexCls() function call:

static <E extends BaseEntity, I extends EntityIndex<E>> boolean isSomeIndexViewable(Class<I> cls)

The problem is that compiler complains about return CoolIndex.class; and return CoolEntity.class; and it's not clear to me why... Of course I can cast it to Class<I> (first case) but it seems to me like I'm trying to mask my misunderstanding and it doesn't feel right.

mr.nothing
  • 5,141
  • 10
  • 53
  • 77

2 Answers2

4

The problem with getIndexCls is that because it's generic, the type parameters can be interpreted to be any classes that fit the bounds on the declarations. You may think that CoolIndex.class fits those bounds, and it does, but a caller of the method can supply their own type arguments which would be incompatible, e.g.:

Entities.COOL_ENTITY.<UncoolEntity, UncoolIndex>getIndexCls();

That would break type safety, so the compiler disallows this. You can cast to Class<I>, but the compiler will warn you about an unchecked cast for the same reason. It will compile, but it can cause runtime problems as I've described.

Other situations can resolve such a situation by passing a Class<I> object to make the type inference work properly, but that defeats the point of this method -- returning a Class<I> object.

Other situations call for moving the generic type parameters from the method to the class, but you are using enums, which can't be generic.

The only way I've come up with to get something similar to compile is by removing the enum entirely. Use an abstract class so you can declare class-level type parameters. Instantiate constants with the type arguments you desire.

abstract class Entities<E extends BaseEntity, I extends EntityIndex<E>> {
    public static final Entities<CoolEntity, CoolIndex> COOL_ENTITY = new Entities<CoolEntity, CoolIndex>() {
        @Override
        public Class<CoolIndex> getIndexCls() {
            return  CoolIndex.class;
        }

        @Override
        public Class<CoolEntity> getEntityCls() {
            return CoolEntity.class;
        }
    };

    // Don't instantiate outside this class!
    private Entities() {}

    public abstract Class<I> getIndexCls();
    public abstract Class<E> getEntityCls();
}
rgettman
  • 176,041
  • 30
  • 275
  • 357
  • Thanks for this solution. It, anyway, looks a little bit dirty, but it is the most "right" solution in this case, I guess. – mr.nothing Apr 26 '16 at 16:47
1

This can be reproduced by much simpler example:

public <E extends BaseEntity> E get() {
    return new BaseEntity(); // compilation error here
}

The problem in such declaration <E extends BaseEntity> is that your method claims to return an instance of any type E that caller should ask:

MyCoolEntity1 e = get(); // valid, E is MyCoolEntity1
MyCoolEntity2 e = get(); // valid, E is MyCoolEntity2

This code should be compile-time safe, so you have to cast result of your method to E

public <E extends BaseEntity> E get() {
    return (E) new BaseEntity(); // no error, but unsafe warning
}

In your example it's pretty the same, you claim to return value of type Class<E>:

public <E extends BaseEntity> Class<E> getEntityCls() 

But return a concrete class SomeEntity.class which is Class<CoolEntity>


OK, how should I fix that?
  1. You can add type cast return (Class<I>) CoolIndex.class; / return (Class<E>) CoolEntity.class;

  2. You can replace enum with classes, since enums can not be generic and classes can

  3. You can entirely remove generics, since there's no much value in it

AdamSkywalker
  • 11,408
  • 3
  • 38
  • 76