Подтвердить что ты не робот

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

Почему выполняется компиляция следующего кода? Метод 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.

4b9b3361

Ответ 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):

  • [& hellip;]

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

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

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

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

  • [& hellip;]

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

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

Позже они будут решены согласно 18.4:

[& hellip;] для каждого αi определен экземпляр-кандидат Ti:

  • В противном случае, где α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, поэтому невозможно преобразовать a 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;
}

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