Невозможно добавить значение в коллекцию 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>
; то есть вы пытаетесь добавить супер тип в список подтипов, который не может работать.