Javaddict
Javaddict

Reputation: 566

Cast doesn't work in java using Class<T> and < T extends A>

I have a strange problem with the Java compiler. Here is the code:

private <T extends IdentifiedBusinessTransversalEntity> T 
    getOrCreateTransversalEntity(Class<T> classT, String id) {
...}

private <T extends IdentifiedBusinessDSEntity> T
    getOrCreateDSEntity(Class<T> classT, String id) {
...}

public abstract  class IdentifiedBusinessDSEntity extends
        BusinessDSEntity implements IdentifiedEntity {
...}

public abstract class IdentifiedBusinessTransversalEntity extends
                BusinessTransversalEntity implements IdentifiedEntity {
 ...}

public <T extends IdentifiedEntity> T getOrCreate(Class<T> classT, String id)
{
    if (IdentifiedBusinessDSEntity.class.isAssignableFrom(classT))
    {
        return getOrCreateDSEntity(classT.asSubclass(IdentifiedBusinessDSEntity.class),id);
    } else if (IdentifiedBusinessTransversalEntity.class.isAssignableFrom(classT))
    {   //must cast explicitly to T here but works well just above and is exactly the same. Strange
        return (T) getOrCreateTransversalEntity(classT.asSubclass(IdentifiedBusinessTransversalEntity.class),id);
    }
    return null;
}

I don't understand why in the getOrCreate function the compiler allows the first return without (T) but not in the second return. The error is:

Type mismatch: cannot convert from IdentifiedBusinessTransversalEntity to T

It should be ok: getOrCreateTRansversalEntity returns a subclass of IdentifiedBusinessTransversalEntity which implements IdentifiedEntity. The strangest is that it is ok for the first return and this is exactly symmetrical. What is the difference?

Upvotes: 1

Views: 1440

Answers (2)

Lii
Lii

Reputation: 12112

That code should not compile.

I believe that you either have made a mistake which led you to believe that it works or that you have bug in your compiler. Both return-statements in getOrCreate give compile errors for me in Eclipse 4.5.1 and javac 1.8.0_45.

Explanation

EDIT: I changed this explanation, I think I misunderstood the problem at fist.

In the explanation I changed the name of the type parameter of getAndCreate to C to avoid confusion with other type parameters.

The problem is that the in the asSubclass method, the information that the class is a subclass of C is lost; the only information that remains is that the class is a subclass of for example IdentifiedBusinessDSEntity.

asSubclass has the following type:

<U> Class<? extends U> asSubclass(Class<U> clazz)

As we can see the original type parameter of the receiver class, T, is not present in the return type.

getOrCreate is declared to return C. That's the reason you need a cast: To reintroduce the type C to the return value.

Alternative asSubclass

We could imagine that asSubclass had the following type:

<U> Class<? extends U & T> asSubclass(Class<U> clazz)

That would be type safe, and with that return type your code would compile without casts. But multiple type parameters as bounds like this is not allowed in Java.

Code

The following is the code I used to investigate the problem:

class Test {
    interface IdentifiedEntity {}
    class BusinessDSEntity {}
    class BusinessTransversalEntity {}

    private <T extends IdentifiedBusinessTransversalEntity> T getOrCreateTransversalEntity(Class<T> classT, String id) {
        return null;
    }

    private <T extends IdentifiedBusinessDSEntity> T getOrCreateDSEntity(Class<T> classT, String id) {
        return null;
    }

    public abstract  class IdentifiedBusinessDSEntity 
        extends BusinessDSEntity implements IdentifiedEntity {}

    public abstract class IdentifiedBusinessTransversalEntity 
        extends BusinessTransversalEntity implements IdentifiedEntity {}

    public <C extends IdentifiedEntity> C getOrCreate(Class<C> classT, String id) {
        if (IdentifiedBusinessDSEntity.class.isAssignableFrom(classT)) {
            // Error here. Note that the type of s does not contain C.
            Class<? extends IdentifiedBusinessDSEntity> s = classT.asSubclass(IdentifiedBusinessDSEntity.class);
            return getOrCreateDSEntity(s, id);
        } else if (IdentifiedBusinessTransversalEntity.class.isAssignableFrom(classT)) {
            // Also error here
            return getOrCreateTransversalEntity(classT.asSubclass(IdentifiedBusinessTransversalEntity.class), id);
        }
        return null;
    } 
}

Possible solution

I can't think of any really elegant solution that preserves much type safety.

One alternative is to pass along the class of the return type to the entity specific methods. This is rather ugly and inconvenient and you have to manually verify that that class really is of a proper type.

It's probably better just to cast the return values, even if you lose some type safety.

Example:

class Test {
    interface IdentifiedEntity {}
    class BusinessDSEntity {}
    class BusinessTransversalEntity {}

    private <R extends IdentifiedEntity, T extends IdentifiedBusinessDSEntity> 
        R getOrCreateDSEntity(Class<T> classT, Class<R> classR, String id)
    {
        // Verify that classT really is subclass of classR.
        classT.asSubclass(classR);
        return null;
    }

    private <R extends IdentifiedEntity, T extends IdentifiedBusinessTransversalEntity> 
        R getOrCreateTransversalEntity(Class<T> classT, Class<R> classR, String id)
    {
        // Verify that classT really is subclass of classR.
        classT.asSubclass(classR);
        return null;
    }

    public abstract  class IdentifiedBusinessDSEntity 
        extends BusinessDSEntity implements IdentifiedEntity {}

    public abstract class IdentifiedBusinessTransversalEntity 
        extends BusinessTransversalEntity implements IdentifiedEntity {}

    public <C extends IdentifiedEntity> C getOrCreate(Class<C> classT, String id) {
        if (IdentifiedBusinessDSEntity.class.isAssignableFrom(classT)) {
            return getOrCreateDSEntity(classT.asSubclass(IdentifiedBusinessDSEntity.class), classT, id);
        } else if (IdentifiedBusinessTransversalEntity.class.isAssignableFrom(classT)) {
            return getOrCreateTransversalEntity(classT.asSubclass(IdentifiedBusinessTransversalEntity.class), classT, id);
        }
        return null;
    }
}

Upvotes: 1

K.Nicholas
K.Nicholas

Reputation: 11551

I'll give it a go. This explanation is based on observation more than explicit knowledge, so don't be surprised if I am corrected. What I think you are confusing is what the <T extends IdentifiedBusinessDSEntity> T return type declaration means. It seems to me that this return declaration is creating an entirely new type, not describing an already declared type. In other words, you can have Class1 as Class1 extends ClassA, but when you return T extends ClassA you are not describing Class1 but an entirely new class (or type). You can't expect this new class to be the same as Class1, rather it is the same as saying you are returning a ClassX where ClassX extends ClassA: Class1 and ClassX are not the same classes.

It looks like you are trying to implement a Factory Design pattern with caching. This code does that and should pretty much resemble what you were doing above. Assuming the factory is supposed to generate Interface implementations, there is no need to declare the return type as a class that extends the interface, just return the interface.

public class Factory {

    interface I {}

    List<I> iCache = new ArrayList<I>();

    abstract class ClassA {}
    abstract class ClassB {}

    class Class1 extends ClassA implements I {}
    class Class2 extends ClassB implements I {}

    I getOrCreateTypeA() { 
        for( I cls: iCache ) {
            if( cls instanceof ClassA ) return cls;
        }
        Class1 cls = new Class1();
        iCache.add(cls);
        return cls;
    }
    I getOrCreateTypeB() { 
        for( I cls: iCache ) {
            if( cls instanceof ClassB ) return cls;
        }
        Class2 cls = new Class2();
        iCache.add(cls);
        return cls;
    }

    I getOrCreate(Class<?> cls) {
        if ( ClassA.class.isAssignableFrom(cls)) {
            return getOrCreateTypeA();
        } else if ( ClassB.class.isAssignableFrom(cls)) {
            return getOrCreateTypeB();
        }
        return null;
    }

    void run() {
        I classI1 = getOrCreate(Class1.class);
        System.out.println(classI1);
        I classI2 = getOrCreate(Class2.class);
        System.out.println(classI2);
        I classI3 = getOrCreate(Class1.class);
        System.out.println(classI3);
        System.out.println(iCache);
    }

    public static void main(String... args) {
        new Factory().run();
    }
}

Upvotes: 0

Related Questions