Nick Lowery
Nick Lowery

Reputation: 187

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

Upvotes: 13

Views: 827

Answers (2)

fukanchik
fukanchik

Reputation: 2865

  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.

Upvotes: 5

scottb
scottb

Reputation: 10084

The Ts aren't exactly the same. Imagine that the interfaces were defined like this instead:

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

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

The Child interface extends the Parent interface, so we can "substitute" the formal type parameter T1 with the "actual" type parameter which we can say is "T2 extends Integer":

interface Parent<<T2 extends Integer> extends Number>

this is only allowed because Integer is a subtype of Number. Therefore, the signature of foo() in the Parent interface (after being extended in the Child interface) is simplified to:

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

In other words, the signature is not changed. The method foo() as declared in the Parent interface continues to return Number as the raw type.

Upvotes: 1

Related Questions