Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why these generics don't compile in OpenJDK7, but do in OpenJDK6

class HasId<I> {}
class HasStringId extends HasId<String> {}
class Alert<T extends /*Some*/Object> extends HasStringId {}
class BaseController<M extends HasId<String>> {
    // abstract Class<M> getModelClass();
}
class AlertController extends BaseController<Alert> { // error here
    // @Override Class<Alert> getModelClass() {
    //     return Alert.class;
    // }
}

compiles fine on OpenJDK6, but in OpenJDK7 gives:

AlertController.java:50: error: type argument Alert is not within bounds of
    type-variable T
class AlertController extends BaseController<Alert> {
                                        ^
  where T is a type-variable:
    T extends HasId<String> declared in class BaseController

Note that there's rawtype warning at line 50, because Alert must be parameterized. If I do that, e.g. extends BaseController<Alert<Object>>, code compiles. But I cannot do that, because I need to implement getModelClass().

UPDATE: That was a bug in Java 6 implementations, which was fixed in Java 7: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6559182. (And here's my question to compiler devs: http://openjdk.5641.n7.nabble.com/Nested-generics-don-t-compile-in-1-7-0-15-but-do-in-1-6-0-27-td121820.html )

like image 443
Dzmitry Lazerka Avatar asked Mar 11 '13 03:03

Dzmitry Lazerka


3 Answers

The question is whether HasId<String> is a supertype of the raw type Alert. The spec is not very clear on this issue.

In the spirit of [4.8], the supertypes of a raw type should all be erased types too. So Alert should have a supertype HasId, but not HasId<String>. However the section talks only in terms of "super classes/interfaces", not in terms of "supertypes".

In the spirit of [4.10], the supertypes are discovered through direct supertypes. It's unclear how the section applies to raw types. It probably intends to rule that raw Alert has a direct supertype HasStringId. That seems fair. Then because HasId<String> is a direct supertype of HasStringId, by transitivity, HasId<String> is a supertype of Alert!

The confusion is rooted in the fact that there are actually two HasStringId types, one normal, one raw. Even though HasStringId is not generic in itself, its has a generic supertype, so it makes sense to talk about the raw version of HasStringId.

The spec does not make a distinction between the normal and raw HasStringId. That's an oversight.

Suppose we denote the raw HasStringId as HasStringId', then [4.10] makes more sense now. The direct super interface of raw Alert is raw HasStringId'. The direct super interface of raw HasStringId' is raw HasId. Therefore HasId is a supertype of Alert, not HasId<String>.

See section 4 of JLS. I'm linking to the prev JLS here, since JLS 7 has serious editing errors in section 4.10.2

like image 87
ZhongYu Avatar answered Oct 14 '22 12:10

ZhongYu


There are a number of documented cases of Java 7 compilers being stricter than Java 6 compilers for various generics nuances. These cases are often related to the actual language spec having become more specific. The error likely has to do with the use of any raw type essentially "opting out" of generics on inherited types - whether it's correct is debatable though.

EDIT: I couldn't find this issue in the list of JDK 7 incompatibilities. The error is reproducible using sun-jdk-1.7.0_10, but not with the Eclipse compiler (which historically has a much better track record than javac when it comes to generics nuances). You should submit an Oracle bug.

Here's a possible workaround:

class AlertController extends BaseController<Alert<?>> {
    @Override
    @SuppressWarnings("unchecked")
    Class<Alert<?>> getModelClass() {
        return (Class<Alert<?>>)(Class<?>)Alert.class;
    }
}
like image 24
Paul Bellora Avatar answered Oct 14 '22 12:10

Paul Bellora


I believe this has to do with how erasure is handled in the absence of actual type parameters. When a parameterized type is referenced without any type parameters, all references to those parameters are erased.

In this instance, you have a parameterized type Alert being used without any type parameter. This erases all type parameters on Alert and its superclasses. This causes the type parameter of HasId in the extends-clause of HasStringId to be erased. Alert then does not subclass HasId<String> because HasStringId no longer extends it but rather extends HasId.

Paul B.'s workaround or the one below avoids this issue by always using Alert with its type parameters.

class AlertController<T> extends BaseController<Alert<T>> {
    @Override Class<Alert<T>> getModelClass() {
        return cast(Alert.class);
    }

    @SuppressWarnings("unchecked")
    private <T> T cast(final Object o) {
        return (T) o;
    }
}
like image 39
oconnor0 Avatar answered Oct 14 '22 11:10

oconnor0