Why subsignature and unchecked rules work this way on return types when overriding a generic method with a non generic one?

public class Base {

     <T> List<? extends Number>  f1()  {return null;}
     List<? extends Number>      f2()  {return null;}
     <T extends Number> List<T>  f3()  {return null; }
}

class Derived extends Base {

     List<String> f1() {return null;}   // compiles fine !!!
     List<String> f3() {return null; }  // compiles fine !!!

    // compile ERR: return type is incompatible with Base.f2()
     List<String> f2() {return null;}   
}

Why definition of overriding methods f1() and f3() in Derived class give no compile error, like definition of overriding f2() method in Derived class (which gives compile error "return type is incompatible with Base.f2()")?

Subsignature override rule in JLS allows overriding method (in Derived class) to be non-generic, while overridden method (in Base class) is generic.

Unchecked override rule allowes to make return type in subclass List<String> instead of List<T> in Base class.

But I cannot explain the difference in behaviour below, and I do not understand why f1() and f3() overriding definitions in Derived class compile successfully (on Eclipse, SE8), ignoring restrictions imposed by bounded type parameter for f3() and by bounded wildcard for f1()!

P.S. My guess - in f1() and f3() in Derived compiler treats both methods as returning just "raw" List - compiler makes erasure (at this moment only in Derived!?) first, and then compares these erased methods in Derived with unerased (so far) methods in Base. Now unchecked overriding rule is OK (and no need to check bounds -it is simply impossible), compiler decides that this is correct overriding and compilation goes further... and somewhere at the end of compilation generics in Base.f1() and Base.f3() also erased :)))

This SO answer also adds ideas to this topic.

Jon Skeet
people
quotationmark

Your f1 and f3 overrides aren't generic, despite the original declarations being generic. The compiles allows the return type of the overrides to vary from the original return types as they have the same type erasure (List). I think that follows from JLS 8.4.5, although frankly I find this part of the spec a bit confusing.

If you change the overrides to be generic again:

<T> List<String> f1() {return null;}
<T extends Number> List<String> f3() {return null; }

... then both fail to compile:

error: <T#1>f1() in Derived cannot override <T#2>f1() in Base
     <T> List<String> f1() {return null;}
                      ^
  return type List<String> is not compatible with List<? extends Number>
  where T#1,T#2 are type-variables:
    T#1 extends Object declared in method <T#1>f1()
    T#2 extends Object declared in method <T#2>f1()

error: <T#1>f3() in Derived cannot override <T#2>f3() in Base
     <T extends Number> List<String> f3() {return null; }
                                     ^
  return type List<String> is not compatible with List<T#1>
  where T#1,T#2 are type-variables:
    T#1 extends Number declared in method <T#1>f3()
    T#2 extends Number declared in method <T#2>f3()

Note that even in your original code, javac issues a warning around unsafe conversions, and if you use -Xlint:unchecked it gives details:

return type requires unchecked conversion from List<String> to List<? extends Number>
return type requires unchecked conversion from List<String> to List<T>

people

See more on this question at Stackoverflow