Search code examples
javagenericsjavacjava-7java-6

Java self type recursive type parameters and inheritance error in javac


Why does this code not compile?

public class x
{
    private void test()
    {
        handle(new ThingA());
        handle(new ModifiedThingA());
    }

    private <T extends BaseThing<T>, X extends T> java.util.List<T> handle(X object)
    {
        return object.getList();
    }

    private static class BaseThing<T extends BaseThing<T>>
    {
        public java.util.List<T> getList()
        {
            return null;
        }
    }

    private static class ThingA
        extends BaseThing<ThingA>
    {
    }

    private static class ModifiedThingA
        extends ThingA
    {
    }
}

Java 6 gives this this error in handle(new ModifiedThingA());:

x.java:6: <T,X>handle(X) in x cannot be applied to (x.ModifiedThingA)
            handle(new ModifiedThingA());
            ^

Java 7 does not even like handle(new ThingA());, this is the Java 7 output:

x.java:5: error: invalid inferred types for T; inferred type does not conform to declared bound(s)
            handle(new ThingA());
                  ^
    inferred: ThingA
    bound(s): CAP#1
  where T,X are type-variables:
    T extends BaseThing<T> declared in method <T,X>handle(X)
    X extends T declared in method <T,X>handle(X)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends BaseThing<CAP#1> from capture of ?
x.java:6: error: invalid inferred types for T; inferred type does not conform to declared bound(s)
            handle(new ModifiedThingA());
                  ^
    inferred: ModifiedThingA
    bound(s): CAP#1
  where T,X are type-variables:
    T extends BaseThing<T> declared in method <T,X>handle(X)
    X extends T declared in method <T,X>handle(X)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends BaseThing<CAP#1> from capture of ?
2 errors

It seems to me that javac is mistaking ModifiedThingA for a BaseThing<ModifiedThingA> when it is in fact a BaseThing<ThingA>. Is this my bug or javac's?


Solution

  • javac's behavior appears to be correct. Theoretically one type variable, T, would be sufficient. However, you introduced a second type variable, X, to help type inference along. The argument for X gets inferred first and then, based on the invocation context, the argument for T is inferred:

    List<ThingA> a = handle(new ThingA());
    List<ThingA> b = handle(new ModifiedThingA());
    

    However, your invocation context does not put any bounds on the return type. Thus the compiler is forced to introduce a type variable (CAP#1) with the null type as lower bound. T's argument will be inferred as glb(BaseThing<CAP#1>) = BaseThing<CAP#1>. Given its lower bound, X is not provably a subtype of T.

    There are two or three ways out of this.

    1. manually infer (okay if rarely necessary)
    2. provide an "overload" with return type void (needs another name, urgh)
    3. if the returned list is immutable or a defensive copy, you can disconnect the type argument T from its bounds argument

    I prefer option 3:

    private <T extends BaseThing<T>> List<T> handle(BaseThing<? extends T> object) {
        return new ArrayList<T>(object.getList());
        // or (using guava's ImmutableList)
        return ImmutableList.copyOf(object.getList());
    }
    

    Happy generics.