Почему этот универсальный метод с привязкой может возвращать любой тип?
Почему следующий код компилируется? Метод IElement.getX(String)
возвращает экземпляр типа IElement
или их подклассов. Код в Main
класс вызывает getX(String)
метод. Компилятор позволяет хранить возвращаемое значение в переменной типа Integer
(что, очевидно, не в иерархии IElement
).
public interface IElement extends CharSequence {
<T extends IElement> T getX(String value);
}
public class Main {
public void example(IElement element) {
Integer x = element.getX("x");
}
}
Не должен ли возвращаемый тип все еще быть экземпляромIElement
- даже после стирания типа?
Байт-код getX(String)
метод это:
public abstract <T extends IElement> T getX(java.lang.String);
flags: ACC_PUBLIC, ACC_ABSTRACT
Signature: #7 // <T::LIElement;>(Ljava/lang/String;)TT;
Изменить: Заменено String
в соответствии с Integer
,
1 ответ
На самом деле это законный тип вывода *.
Мы можем свести это к следующему примеру ( Ideone):
interface Foo {
<F extends Foo> F bar();
public static void main(String[] args) {
Foo foo = null;
String baz = foo.bar();
}
}
Компилятору разрешено выводить (бессмысленный, действительно) тип пересечения String & Foo
так как Foo
это интерфейс. Для примера в вопросе, Integer & IElement
выводится
Это бессмысленно, потому что преобразование невозможно. Мы не можем сделать такой бросок сами:
// won't compile because Integer is final
Integer x = (Integer & IElement) element;
Вывод типа в основном работает с:
- набор переменных логического вывода для каждого из параметров типа метода.
- набор границ, которые должны соответствовать.
- иногда ограничения, которые сводятся к границам.
В конце алгоритма каждая переменная преобразуется в тип пересечения на основе набора границ, и, если они действительны, вызов компилируется.
Процесс начинается в 8.1.3:
Когда начинается логический вывод, связанный набор обычно генерируется из списка объявлений параметров типа
P1, ..., Pp
и связанные переменные логического выводаα1, ..., αp
, Такое связанное множество строится следующим образом. Для каждого l (1 ≤ l ≤ p):
[...]
В противном случае для каждого типа
T
разграничены&
в TypeBound, границаαl <: T[P1:=α1, ..., Pp:=αp]
появляется в наборе […].
Итак, это означает, что сначала компилятор начинает с границы F <: Foo
(что значит F
это подтип Foo
).
Переходя к 18.5.2, возвращаемый тип цели рассматривается:
Если вызов является поли-выражением, […] пусть
R
быть типом возвратаm
, позволятьT
быть целевым типом вызова, а затем:
[...]
В противном случае формула ограничения
‹R θ → T›
сокращается и включается в [связанный набор].
Формула ограничения ‹R θ → T›
сводится к другому пределу R θ <: T
Итак, мы имеем F <: String
,
Позже они решаются в соответствии с 18.4:
[…] Кандидатский экземпляр
Ti
определяется для каждогоαi
:
- В противном случае, где
αi
имеет правильные верхние границыU1, ..., Uk
,Ti = glb(U1, ..., Uk)
,Границы
α1 = T1, ..., αn = Tn
включены в текущий набор ограничений.
Напомним, что наш набор границ F <: Foo, F <: String
, glb(String, Foo)
определяется как String & Foo
, Это, очевидно, допустимый тип для glb, который требует только, чтобы:
Это ошибка времени компиляции, если для любых двух классов (не интерфейсов)
Vi
а такжеVj
,Vi
не подклассVj
или наоборот.
В заключение:
Если разрешение успешно с экземплярами
T1, ..., Tp
для переменных выводаα1, ..., αp
, позволятьθ'
быть заменой[P1:=T1, ..., Pp:=Tp]
, Затем:
- Если неконтролируемое преобразование не было необходимым для применимости метода, тогда тип вызова
m
получается путем примененияθ'
к типуm
,
Поэтому метод вызывается с String & Foo
как тип F
, Мы можем, конечно, назначить это String
таким образом невозможно преобразовать Foo
к String
,
Дело в том, что String
/ Integer
Окончательные занятия, видимо, не рассматриваются.
* Примечание: стирание типа полностью не связано с проблемой.
Кроме того, хотя это компилируется и в Java 7, я думаю, что разумно сказать, что нам не нужно беспокоиться о спецификации там. Вывод типа Java 7 был по сути менее сложной версией Java 8. Он компилируется по аналогичным причинам.
Как дополнение, хотя это и странно, это, вероятно, никогда не вызовет проблемы, которой еще не было. Редко полезно писать универсальный метод, тип возвращаемого значения которого определяется исключительно из целевого значения возврата, поскольку только null
можно вернуть из такого метода без приведения.
Предположим, например, что у нас есть некоторый аналог карты, который хранит подтипы определенного интерфейса:
interface FooImplMap {
void put(String key, Foo value);
<F extends Foo> F get(String key);
}
class Bar implements Foo {}
class Biz implements Foo {}
Уже вполне допустимо сделать ошибку, такую как следующее:
FooImplMap m = ...;
m.put("b", new Bar());
Biz b = m.get("b"); // casting Bar to Biz
Так что тот факт, что мы также можем сделать Integer i = m.get("b");
не новая возможность для ошибки. Если бы мы программировали код, подобный этому, это было бы потенциально несостоятельным с самого начала.
Как правило, параметр типа следует выводить только из целевого типа только в том случае, если нет причин для его привязки, например Collections.emptyList()
а также Optional.empty()
:
private static final Optional<?> EMPTY = new Optional<>();
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
Это нормально, потому что Optional.empty()
не может ни производить, ни потреблять T
,