Java automatic return type covariance with generic subclassing

Nick Lowery :

I have two interfaces that look like this:

interface Parent<T extends Number> {
    T foo();
}

interface Child<T extends Integer> extends Parent<T> {
}

If I have a raw Parent object, calling foo() defaults to returning a Number since there is no type parameter.

Parent parent = getRawParent();
Number result = parent.foo(); // the compiler knows this returns a Number

This makes sense.

If I have a raw Child object, I would expect that calling foo() would return an Integer by the same logic. However, the compiler claims that it returns a Number.

Child child = getRawChild();
Integer result = child.foo(); // compiler error; foo() returns a Number, not an Integer

I can override Parent.foo() in Child to fix this, like so:

interface Child<T extends Integer> extends Parent<T> {
    @Override
    T foo(); // compiler would now default to returning an Integer
}

Why does this happen? Is there a way to have Child.foo() default to returning an Integer without overriding Parent.foo()?

EDIT: Pretend Integer isn't final. I just picked Number and Integer as examples, but obviously they weren't the best choice. :S

fukanchik :
  1. This is based on ideas of @AdamGent .
  2. Unfortunately I am not fluent with JLS enough to prove the below from the spec.

Imagine public interface Parent<T extends Number> was defined in a different compilation unit - in a separate file Parent.java.

Then, when compiling Child and main, the compiler would see method foo as Number foo(). Proof:

import java.lang.reflect.Method;
interface Parent<T extends Number> {
    T foo();
}

interface Child<R extends Integer> extends Parent<R> {
}

public class Test {
    public static void main(String[] args) throws Exception {
        System.out.println(Child.class.getMethod("foo").getReturnType());
    }
}

prints:

class java.lang.Number

This output is reasonable as java does type erasure and is not able to retain T extends in the result .class file plus because method foo() is only defined in Parent. To change the result type in the child compiler would need to insert a stub Integer foo() method into the Child.class bytecode. This is because there remains no information about generic types after compilation.

Now if you modify your child to be:

interface Child<R extends Integer> extends Parent<R> {
    @Override R foo();
}

e.g. add own foo() into the Child the compiler will create Child's own copy of the method in the .class file with a different but still compatible prototype Integer foo(). Now output is:

class java.lang.Integer

This is confusing of course, because people would expect "lexical visibility" instead of "bytecode visibility".

Alternative is when compiler would compile this differently in two cases: interface in the same "lexical scope" where compiler can see source code and interface in a different compilation unit when compiler can only see bytecode. I don't think this is a good alternative.

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=453459&siteId=1