Невозможно добавить значение в коллекцию Java с подстановочным общим типом

Почему этот код не компилируется (Parent - это интерфейс)?

List<? extends Parent> list = ...
Parent p = factory.get();   // returns concrete implementation
list.set(0, p);   // fails here: set(int, ? extends Parent) cannot be applied to (int, Parent)

Ответы

Ответ 1

Это делает это ради безопасности. Представьте, если бы это сработало:

List<Child> childList = new ArrayList<Child>();
childList.add(new Child());

List<? extends Parent> parentList = childList;
parentList.set(0, new Parent());

Child child = childList.get(0); // No! It not a child! Type safety is broken...

Значение List<? extends Parent>: "Это список какого-либо типа, который расширяет Parent. Мы не знаем, какой тип - это может быть List<Parent>, a List<Child> или List<GrandChild>". Это позволяет безопасно извлекать любые элементы из API List<T> и конвертировать из T в Parent, но небезопасно обращаться к API List<T>, преобразующемуся из Parent в T. потому что это преобразование может быть недействительным.

Ответ 2

List<? super Parent>

PECS - "Производитель - Расширяется, Потребитель - Супер". Ваш List является потребителем Parent объектов.

Ответ 3

Вот мое понимание.

Предположим, что мы имеем общий тип с двумя методами

type L<T>
    T get();
    void set(T);

Предположим, что мы имеем супер тип P, и он имеет подтипы C1, C2 ... Cn. (для удобства мы говорим, что P является подтипом самого себя и на самом деле является одним из Ci)

Теперь мы также получили n конкретных типов L<C1>, L<C2> ... L<Cn>, как если бы мы вручную написали n типов:

type L_Ci_
    Ci get();
    void set(Ci);

Нам не пришлось вручную писать их, что точка. Между этими типами нет отношений

L<Ci> oi = ...;
L<Cj> oj = oi; // doesn't compile. L<Ci> and L<Cj> are not compatible types. 

Для шаблона С++ это конец истории. Это в основном макрораспределение - основано на одном классе "шаблон", оно генерирует много конкретных классов, без каких-либо отношений типов между ними.

Для Java там больше. Мы также получили тип L<? extends P>, это супер тип любого L<Ci>

L<Ci> oi = ...;
L<? extends P> o = oi; // ok, assign subtype to supertype

Какой метод должен существовать в L<? extends P>? Как супер тип, любой из его методов должен быть опознан подтипами. Этот метод будет работать:

type L<? extends P>
    P get();

потому что в любом из его подтипов L<Ci> существует метод Ci get(), который совместим с P get() - метод переопределения имеет тот же сигнатурный и ковариантный тип возврата.

Это не работает для set(), хотя - мы не можем найти тип X, так что void set(X) может быть переопределен void set(Ci) для любого Ci. Поэтому метод set() не существует в L<? extends P>.

Также есть a L<? super P>, который идет в другую сторону. Он имеет set(P), но не get(). Если Si является супер-типом P, L<? super P> является супер-типом L<Si>.

type L<? super P>
    void set(P);

type L<Si>
    Si get();
    void set(Si);

set(Si) "переопределяет" set(P) не в обычном смысле, но компилятор может видеть, что любой действительный вызов на set(P) является действительным вызовом на set(Si)

Ответ 4

Это из-за "конверсии захвата", которая происходит здесь.

Каждый раз, когда компилятор увидит подстановочный тип - он заменит его на "захват" (в ошибках компилятора это обозначается как CAP#1), таким образом:

List<? extends Parent> list

станет List<CAP#1> где CAP#1 <: Parent, где запись <: означает подтип Parent (также Parent <: Parent).

Компилятор java-12, когда вы делаете что-то вроде ниже, показывает это в действии:

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

Среди сообщения об ошибке вы увидите:

.....
CAP#1 extends Parent from capture of ? extends Parent
.....

Когда вы извлекаете что-то из list, вы можете назначить это только на Parent. Если, теоретически, язык Java позволит объявить этот CAP#1, вы можете назначить этому list.get(0), но это не разрешено. Поскольку CAP#1 является подтипом Parent, назначение виртуального CAP#1, который производит этот list, для Parent (супертипа) более чем нормально. Это как делать:

String s = "s";
CharSequence s = s; // assign to the super type

Теперь, почему вы не можете сделать list.set(0, p)? Помните, что ваш список имеет тип CAP#1 и вы пытаетесь добавить Parent в List<CAP#1>; то есть вы пытаетесь добавить супер тип в список подтипов, который не может работать.