Почему этот общий метод с привязкой возвращает какой-либо тип?

Почему компилируется следующий код? Метод IElement.getX(String) возвращает экземпляр типа IElement или его подclassов. Код в основном classе вызывает метод getX(String) . Компилятор позволяет сохранить возвращаемое значение переменной типа Integer (которая явно не находится в иерархии IElement ).

 public interface IElement extends CharSequence {  T getX(String value); } public class Main { public void example(IElement element) { Integer x = element.getX("x"); } } 

Не должен ли возвращаемый тип быть экземпляром IElement – даже после стирания типа?

Байт-код метода getX(String) :

 public abstract  T getX(java.lang.String); flags: ACC_PUBLIC, ACC_ABSTRACT Signature: #7 // (Ljava/lang/String;)TT; 

Изменить: заменить String последовательно на Integer .

Это действительно законный тип вывода *.

Мы можем свести это к следующему примеру ( Идеал ):

 interface 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 :

Когда начинается вывод, связанный набор обычно генерируется из списка объявлений параметров типа P 1 , ..., P p и связанных переменных вывода α 1 , ..., α p . Такое связанное множество строится следующим образом. Для каждого l (1 ≤ l ≤ p) :

  • […]

  • В противном случае для каждого типа T ограниченного символом & в TypeBound , в наборе […] появляется оценка α l <: T[P 1 :=α 1 , ..., P p :=α p ] ].

Итак, это означает, что сначала компилятор начинается с привязки F <: Foo (что означает, что F является подтипом Foo ).

Переходя к 18.5.2 , рассматривается тип возвращаемого объекта:

Если вызов является поли-выражением, [...] пусть R - тип возврата m , T - целевой тип вызова, а затем:

  • [...]

  • В противном случае формула ограничения ‹R θ → T› уменьшается и включается в [связанный набор].

Формула ограничения ‹R θ → T› сводится к другой оценке R θ <: T , поэтому мы имеем F <: String .

Позже они решаются согласно 18.4 :

[...] экземпляр-кандидат T i определен для каждого α i :

  • В противном случае, где α i имеет собственные верхние границы U 1 , ..., U k , T i = glb(U 1 , ..., U k ) .

Оценки α 1 = T 1 , ..., α n = T n включены в текущий связанный набор.

Напомним, что наш набор оценок F <: Foo, F <: String . glb(String, Foo) определяется как String & Foo . Это, по-видимому, законный тип для glb , который требует только:

Это ошибка времени компиляции, если для любых двух classов ( не интерфейсов ) V i и V j V i не является подclassом V j или наоборот.

В заключение:

Если разрешение преуспевает с экземплярами T 1 , ..., T p для переменных вывода α 1 , ..., α p , пусть θ' - подстановка [P 1 :=T 1 , ..., P p :=T p ] . Затем:

  • Если необработанное преобразование не было необходимым для применения метода, то тип вызова m получается путем применения θ' к типу m .

Поэтому метод ссылается на String & Foo как тип F Разумеется, мы можем назначить это String , поэтому невозможно преобразовать Foo в String .

Тот факт, что String / Integer являются конечными classами, по-видимому, не рассматривается.


* Примечание: стирание типа / полностью не связано с проблемой.

Кроме того, хотя это компилируется и на Java 7, я думаю, что разумно сказать, что нам не нужно беспокоиться о спецификации там. Вывод типа Java 7 был по существу менее сложной версией Java 8. Он компилируется по тем же причинам.


Как добавление, хотя и странно, это, вероятно, никогда не вызовет проблемы, которая еще не была. Редко полезно писать общий метод, возвращаемый тип которого выведен исключительно из возвращаемой цели, так как только null может быть возвращен из такого метода без кастинга.

Предположим, например, что у нас есть некоторый аналог карты, в котором хранятся подтипы определенного интерфейса:

 interface FooImplMap { void put(String key, Foo value);  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 Optional empty() { @SuppressWarnings("unchecked") Optional t = (Optional) EMPTY; return t; } 

Это A-OK, потому что Optional.empty() может ни производить, ни потреблять T

Interesting Posts
Давайте будем гением компьютера.