Ковариация с автоматическим возвратом 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
в качестве исходного типа.