Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 495
Nick Lowery Avatar asked Jul 07 '16 16:07

Nick Lowery


2 Answers

  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.

like image 85
fukanchik Avatar answered Oct 20 '22 00:10

fukanchik


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.

like image 42
scottb Avatar answered Oct 19 '22 23:10

scottb