Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 371
Jesse Avatar asked Aug 21 '12 15:08

Jesse


2 Answers

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.

like image 138
Ben Schulz Avatar answered Nov 08 '22 20:11

Ben Schulz


Your code compiles fine in javac 1.8.0_45 and Eclipse 4.1.1.

Probably the changes that where made to the type inference algorithm in Java 8 in order to compile lambda expressions in a nice way also solved your problem.

like image 28
Lii Avatar answered Nov 08 '22 18:11

Lii