Nick Lowery Nick Lowery - 4 months ago 14
Java Question

Java automatic return type covariance with generic subclassing

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

Answer
  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.

Comments