Я думал, что у меня разумное понимание дженериков. Например, я понимаю, почему
Но оба компилируются. Зачем? Какая разница с примером сверху?
Я хотел бы получить объяснение на общем английском языке, а также указатель на соответствующие части спецификации Java или аналогичные.
Ответ 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, ?>
:
..., где для 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>>
не является.