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

Генералы второго порядка, похоже, ведут себя иначе, чем дженерики первого порядка

Я думал, что у меня разумное понимание дженериков. Например, я понимаю, почему

private void addString(List<? extends String> list, String s) {
    list.add(s); // does not compile
    list.add(list.get(0)); // doesn't compile either
}

Не компилируется. Я даже заработал некоторую интернет-карму со знаниями.

Но я бы подумал по тому же аргументу, что это не должно компилироваться:

private void addClassWildcard(List<Class<? extends String>> list, Class<? extends String> c) {
    list.add(c);
    list.add(list.get(0));
}

И это не должно:

private void addClass(List<Class<? extends String>> list, Class<String> c) {
    list.add(c);
    list.add(list.get(0));
}

Но оба компилируются. Зачем? Какая разница с примером сверху?

Я хотел бы получить объяснение на общем английском языке, а также указатель на соответствующие части спецификации Java или аналогичные.

4b9b3361

Ответ 1

Второй случай безопасен, потому что все экземпляры Class<String> являются экземплярами Class<? extends String>.

Нет ничего опасного в добавлении экземпляра Class<? extends String> в List<Class<? extends String> - вы вернете экземпляр Class<? extends String> с помощью get(int), iterator() и т.д., чтобы он разрешался.


В некотором смысле подстановочный знак внутри Class рассматривается только тогда, когда экземпляр этого объекта действительно встречается. Рассмотрим следующие примеры (переход от String в Number, так как String является окончательным).

private void addClass(List<Class<? extends Number>> list, Class<Number> c) {
    list.add(c);
    list.add(list.get(0));
}

private void tryItSubclass() {
    List<Class<Integer>> ints = new ArrayList<>();

    addClass(ints, Number.class); // does not compile 
}

Здесь ints может содержать только экземпляры Class<Integer>, но Number.class также является Class<? extends Number> с ?, записанным как Number, поэтому два типа несовместимы.

private void tryItBound() {
    List<Class<Number>> ints = new ArrayList<>();

    addClass(ints, Number.class); // does not compile
}

Здесь ints может содержать только экземпляры Class<Number>, но Integer.class также является Class<? extends Number> с ?, записанным как Integer, поэтому два типа несовместимы.

private void tryItWildcard() {
    List<Class<? extends Number>> ints = new ArrayList<>();

    addClass(ints, Number.class); // does compile

    Class<? extends Number> aClass = ints.get(0);
}

Первый случай небезопасен, потому что - был ли гипотетический класс, который расширил String (которого нет, потому что String есть final, однако, дженерики игнорируют final), a List<? extends String> может a List<HypotheticalClass>. Таким образом, вы не можете добавить String в List<? extends String>, потому что вы ожидаете, что все в этом списке будет экземпляром HypotheticalClass:

List<HypotheticalClass> list = new ArrayList<>();
List<? extends String> list2 = list;
list2.add("");  // Not allowed, but pretend it is.
HypotheticalClass h = list.get(0);  // ClassCastException.

Ответ 2

Это связано с преобразованием захвата. Ответ Энди велик, но он не объясняет, как работает спецификация. Мой ответ здесь длинный, потому что, ну, это довольно плотная часть JLS, но я не вижу, чтобы это объяснялось много, и это не так сложно, если вы пройдете через него шаг за шагом.

Преобразование захвата - это процесс, при котором компилятор принимает тип с подстановочными знаками и заменяет (некоторые) подстановочные знаки типами, которые не являются подстановочными знаками.

Супертипы параметризованного типа с помощью подстановочных знаков являются супертипами этого типа после преобразования захвата:

4.10.2. Подтипирование между классами и типами интерфейсов

Учитывая объявление универсального типа C<F1,...,Fn> (n > 0), прямые супертипы параметризованного типа C<R1,...,Rn>, где по крайней мере один из Ri (1 ≤ я ≤ n) является аргументом типа подстановочных знаков, являются прямые супертипы параметризованного типа C<X1,...,Xn>, который является результатом применения преобразования захвата в C<R1,...,Rn>.

Типы членов (включая методы) параметризованного типа с помощью подстановочных знаков - это типы членов этого типа после преобразования захвата:

4.5.2. Члены и конструкторы параметризованных типов

Пусть C будет общим объявлением класса или интерфейса с параметрами типа A1,...,An и пусть C<T1,...,Tn> будет параметризацией C, где для 1 ≤ я ≤ n, Ti является типом (скорее чем подстановочный знак). Тогда:

  • [пропущен для неуместности]

Если какой-либо из аргументов типа в параметризации C являются подстановочными знаками, то:

  • Типы полей, методов и конструкторов в C<T1,...,Tn> - это типы полей, методов и конструкторов в преобразовании захвата C<T1,...,Tn>.

Итак, как работает преобразование захвата?

Предположим, что нам дано следующее объявление класса (более полно проиллюстрировано некоторые части процесса):

class C<V, W extends List<V>> {

    void m(V v, W w) {
    }
}

И следующее использование этого типа:

C<Number, ?> c = new C<>();

Double       tArg = 1.0;
List<Number> uArg = new ArrayList<>();
c.m(tArg, uArg);

Как определить тип c.m с целью определения если типы аргументов могут быть назначены типам параметров?

Ну, для начала, как указано выше, типы параметров c.m являются типами параметров m в преобразовании захвата C<Number, ?>:

5.1.10. Преобразование захвата

Пусть G укажите объявление общего типа с параметрами n типа A1,...,An с соответствующими границами U1,...,Un.

В этом примере:

  • G C.
  • A1 имеет V с привязкой U1, которая Object.
  • A2 имеет W с привязкой U2, которая List<V>.

Существует преобразование захвата из параметризованного типа G<T1,...,Tn> в параметризованный тип G<S1,...,Sn>...

В этом примере G<T1,...,Tn> есть C<Number, ?>:

  • T1 - Number.
  • T2 - ?.

..., где для 1 ≤ я ≤ n:

  • Если Ti является аргументом типа подстановки формы ?, то Si является переменной нового типа, верхняя граница которой Ui[A1:=S1,...,An:=Sn] и нижняя граница которой - тип null.

  • Если Ti является аргументом типа подстановки формы ? extends Bi, то Si является переменной нового типа, верхняя граница которой glb(Bi, Ui[A1:=S1,...,An:=Sn]) и нижняя граница которой - тип null.

    glb(V1,...,Vm) определяется как V1 & ... & Vm.

Ui[A1:=S1,...,An:=Sn] является границей Ai (параметр типа) с подстановкой каждого аргумента типа для каждого соответствующего параметра типа. (Вот почему я объявил C параметром типа, связанный с которым ссылается на другой параметр типа: потому что он иллюстрирует, что делает эта часть.)

В нашем примере для T2 (который есть ?), S2 является переменной нового типа, верхняя граница которой U2 (которая есть List<V>) с заменой Number для V.

S2, следовательно, является новой переменной типа, верхняя граница которой List<Number>.

Для простоты я проигнорирую случай, когда у нас ограниченный подстановочный знак, но ограниченный подстановочный знак - это, по сути, просто захват, преобразованный в новую переменную типа, граница которой BoundOfWildcard & BoundOfTypeParameter. Кроме того, если подстановочный знак имеет нижнюю границу (super), то новая переменная типа также имеет нижнюю границу.

Если Ti не является подстановочным знаком, то:

  • В противном случае Si = Ti.

Итак, в нашем примере S1 является просто T1, который является Number.

И что:

Преобразование захвата не применяется рекурсивно.

который мы получим позже.

Теперь мы знаем, что:

  • S1 - Number.
  • S2 - это некоторая переменная типа FRESH extends List<Number>, которую только что создал компилятор.

Следовательно, преобразование захвата C<Number, ?> составляет C<Number, FRESH>.

Теперь мы можем ответить на вопрос: есть ли Double и List<Number>, назначаемые Number и FRESH extends List<Number>, соответственно? В первом случае да. В последнем случае нет.

Это по тем же причинам, что выражение не будет компилироваться, если мы сами объявили переменную типа:

static <FRESH extends List<Number>> void n() {
    C<Number, FRESH> c = new C<>();

    Double       tArg = 1.0;
    List<Number> uArg = new ArrayList<>();
    c.m(tArg, uArg);
}

Супертипы переменной типа::

  • Прямыми супертипами переменной типа являются типы, перечисленные в ее привязке.

Следовательно, List<Number> не может быть присвоен FRESH, потому что List<Number> является супертипом FRESH.

По аналогии, мы могли бы также объявить класс следующим образом:

class Fresh extends List<Number> {}
C<Number, Fresh> c = new C<>();

Double       tArg = 1.0;
List<Number> uArg = new ArrayList<>();
c.m(tArg, uArg);

Это может быть более знакомым, и на самом деле это не так уж и отличается в отношении того, как отношения между типами работают в этом случае.

Другими словами, в нашем первоначальном примере:

C<Number, ?> c = new C<>();

Double       tArg = 1.0;
List<Number> uArg = new ArrayList<>();
c.m(tArg, uArg);
//        ^^^^ this

- это более сложная версия:

Object o = ...;
String s = o; // Error: attempting to assign a supertype to its subtype.

и (в конце дня) не компилируется по той же причине.

В резюме

Преобразование захвата принимает подстановочные знаки и включает их для ввода переменных (временно). После этого это просто обычные правила подтипирования, которые вызывают эти ошибки.

Так, например, учитывая код в вопросе:

private void addString(List<? extends String> list, String s) {
    list.add(s); // does not compile
    list.add(list.get(0)); // doesn't compile either
}

При просмотре выражения list.add(s) компилятор видит что-то вроде этого:

private <CAP#1 extends String>
void addString(List<? extends String> list, String s) {
    ((List<CAP#1>) list).add( s );
    list.add(list.get(0));
}

Полученная ошибка выглядит следующим образом:

error: no suitable method found for add(String)
        list.add(s); // does not compile
            ^
    method Collection.add(CAP#1) is not applicable
      (argument mismatch; String cannot be converted to CAP#1)
    method List.add(CAP#1) is not applicable
      (argument mismatch; String cannot be converted to CAP#1)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends String from capture of ? extends String

Другими словами, обнаруженные компилятором методы add(CAP#1) и String не поддаются изменению переменной типа CAP#1.

При просмотре выражения list.add(list.get(0)) компилятор видит что-то вроде этого:

private <CAP#1 extends String, CAP#2 extends String>
void addString(List<? extends String> list, String s) {
    list.add(s);
    ((List<CAP#2>) list).add( ((List<CAP#1>) list).get(0) );
}

Полученная ошибка выглядит следующим образом:

error: no suitable method found for add(CAP#1)
        list.add(list.get(0)); // doesn't compile either
            ^
    method Collection.add(CAP#2) is not applicable
      (argument mismatch; String cannot be converted to CAP#2)
    method List.add(CAP#2) is not applicable
      (argument mismatch; String cannot be converted to CAP#2)
  where CAP#1,CAP#2 are fresh type-variables:
    CAP#1 extends String from capture of ? extends String
    CAP#2 extends String from capture of ? extends String

Другими словами, компилятор обнаружил, что list.get(0) возвращает CAP#1 и нашел методы add(CAP#2), но CAP#1 не может быть преобразован в CAP#2.

(Источник ошибок.)

Итак, почему работает List<Class<?>> и другие подобные типы?

Напомним, что:

  • В противном случае, если Ti не является подстановочным шрифтом], Si = Ti.

И что:

Преобразование захвата не применяется рекурсивно.

Итак, если Ti является параметризованным типом типа Class<?>, то Si является просто Class<?>. Кроме того, поскольку преобразование захвата не применяется рекурсивно, алгоритм просто останавливается после преобразования T1,...,Tn в S1,...,Sn. Новый тип не конвертируется с захватом, а границы переменных нового типа не преобразуются в захват.

Мы также можем убедиться, что это действительно то, что делает компилятор, вызывая некоторые интересные ошибки:

Map<?, List<?>> m = new HashMap<>();

List<?> list = new ArrayList<>();
list.add(m);

Это приводит к следующей ошибке:

error: no suitable method found for add(Map<CAP#1,List<?>>)
        list.add(m);
            ^
    […]

(Источник.

Обратите внимание, что аргумент типа List<?> в захвате типа Map преобразует в себя.

И еще:

Map<?, ? extends List<?>> m = new HashMap<>();

List<?> list = new ArrayList<>();
list.add(m);

Это приводит к следующей ошибке:

error: no suitable method found for add(Map<CAP#1,CAP#2>)
        list.add(m);
            ^
    […]
  where CAP#1,CAP#2,CAP#3 are fresh type-variables:
    CAP#1 extends Object from capture of ?
    CAP#2 extends List<?> from capture of ? extends List<?>
    CAP#3 extends Object from capture of ?

(Источник.

Обратите внимание, что на этот раз, когда ? extends List<?> преобразуется с захватом, граница List<?> не является.

Наконец

Ответ на поставленный вопрос заключается в том, что подстановочный знак в List<? extends String> преобразуется с захватом в новую переменную типа, но подстановочный знак в List<Class<? extends String>> не является.

Ответ 3

Ваш пример пропускает факт (по крайней мере, я так думаю), что (переход к Integer и Number для существующих примеров) List<Class<Integer>> не является допустимым экземпляром List<Class<? extends Number>>.

Итак, это не скомпилируется:

public static void main(String[] args) {
    List<Class<Integer>> intClasses = new LinkedList<>();
    addClass(intClasses, Number.class); // compiler error
}

private static void addClass(List<Class<? extends Number>> list, Class<Number> c) {
    list.add(c);
    list.add(list.get(0));
}