Ковариация с автоматическим возвратом Java с общим подклассом

У меня есть два интерфейса, которые выглядят так:

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

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

Если у меня есть необработанный объект Parent, вызов foo() по умолчанию возвращает Number, поскольку нет параметра типа.

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

Это имеет смысл.

Если у меня есть исходный объект Child, я бы ожидал, что вызов foo() вернет Integer по той же логике. Однако компилятор утверждает, что он возвращает Number.

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

Я могу переопределить Parent.foo() в Child, чтобы исправить это, например:

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

Почему это происходит? Есть ли способ, чтобы Child.foo() по умолчанию возвращал Integer без переопределения Parent.foo()?

РЕДАКТИРОВАТЬ: Притвориться Integer не является окончательным. Я просто выбрал Number и Integer в качестве примеров, но, очевидно, это был не лучший выбор.: S

Ответ 1

  • Это основано на идеях @AdamGent.
  • К сожалению, я недостаточно свободно разбираюсь в JLS, чтобы доказать это ниже из спецификации.

Представьте, что public interface Parent<T extends Number> был определен в другом компиляторе - в отдельном файле Parent.java.

Затем при компиляции Child и main компилятор увидит метод foo как Number foo(). Доказательство:

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());
    }
}

печатает:

class java.lang.Number

Этот вывод является разумным, поскольку java стирает стирание и не может сохранить T extends в файле результата .class плюс, потому что метод foo() определяется только в Parent. Чтобы изменить тип результата в дочернем компиляторе, необходимо вставить метод stub Integer foo() в байт-код Child.class. Это связано с тем, что после компиляции информация о генерических типах отсутствует.

Теперь, если вы измените своего ребенка:

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

например. добавьте собственный foo() в Child, компилятор создаст собственную копию метода Child Child в файле .class с другим, но все еще совместимым прототипом Integer foo(). Теперь вывод:

class java.lang.Integer

Это сбивает с толку, конечно, потому что люди будут ожидать "лексической видимости" вместо "видимости байт-кода".

Альтернатива заключается в том, что компилятор будет скомпилировать это по-другому в двух случаях: интерфейс в той же "лексической области", где компилятор может видеть исходный код и интерфейс в другом блоке компиляции, когда компилятор может видеть только байт-код. Я не думаю, что это хорошая альтернатива.

Ответ 2

T не совсем то же самое. Представьте, что интерфейсы были определены следующим образом:

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

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

Интерфейс Child расширяет интерфейс Parent, поэтому мы можем "заменить" формальный параметр типа T1 параметром "фактического" типа, который мы можем сказать: "T2 extends Integer":

interface Parent<<T2 extends Integer> extends Number>

это разрешено только потому, что Integer является подтипом Number. Поэтому подпись foo() в интерфейсе Parent (после расширения в интерфейсе Child) упрощена до:

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

Другими словами, подпись не изменяется. Метод foo(), объявленный в интерфейсе Parent, продолжает возвращать Number в качестве исходного типа.