Вопрос по generic-method, generics, type-inference, java – Почему этот универсальный метод с привязкой может возвращать любой тип?

33

Почему следующий код компилируется? Метод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.

Версия Java: 1.7.0_45 32 бит nrainer
Только что протестировал оригинальную версиюInteger) с Java 1.8.0_31 - он действительно компилируется. Ray
Кажется, имеет отношение к использованию интерфейса в качестве верхней границы. Изменить на "T расширяет список", все еще компилируется .. Tobb
Он также компилируется с Java 6. Странно, кажется, принять любой тип возврата. Строка str = element.getX ("Y") также работает. И когда вы выделяете метод в затмении, он показывает <? расширить Integer>? расширяет целочисленный IElement.getX (String) ramp
CharSequence назначается на String .. в чем проблема? Muhammad Hamed

Ваш Ответ

1   ответ
21

На самом деле это законный тип вывода *.

Мы можем свести это к следующему примеру (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:

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

[...]

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

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

Переход к18.5.2тип возвращаемого целевого объекта учитывается:

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

[...]

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

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

Позже они решаются в соответствии с18,4:

[…] Кандидат на созданиеT<sub>i</sub> определяется для каждогоα<sub>i</sub>:

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

Границыα<sub>1</sub> = T<sub>1</sub>, ..., α<sub>n</sub> = T<sub>n</sub> включены в текущий набор ограничений.

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

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

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

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

Если неконтролируемое преобразование не было необходимым для применимости метода, тогда тип вызова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.

@Radiodef -Сырые Типы!! Это была фраза, которую я искал! OldCurmudgeon
@ OldCurmudgeon Я могу заверить вас, что в этом разделе вопросов и ответов сейчас нет необработанных типов. А если бы и было, в любом случае происходит другое. Мы на самом деле получаем ошибку.ideone.com/SeZ09H Radiodef
Тем не менее, вы можете кастовать изObject в(Integer&IElement) :) или изNumber и т.п. ZhongYu
Проблема может быть в том, чтоT extends [interface] неявно означаетT extends Object & [Interface], Поскольку String является Object, компилятор не сообщает о проблеме. Clashsoft
Большое спасибо за описание этой загадочной части JLS, чтобы объяснить, что происходит. benzonico

Похожие вопросы