Несколько подстановочных знаков в универсальных методах делают Java-компилятор (и меня!) Очень запутанным
Давайте сначала рассмотрим простой сценарий ( см. Полный исходный код на ideone.com):
import java.util.*;
public class TwoListsOfUnknowns {
static void doNothing(List<?> list1, List<?> list2) { }
public static void main(String[] args) {
List<String> list1 = null;
List<Integer> list2 = null;
doNothing(list1, list2); // compiles fine!
}
}
Два подстановочных знака не связаны, поэтому вы можете позвонить doNothing
с List<String>
и List<Integer>
, Другими словами, два ?
могут относиться к совершенно разным типам. Следовательно, следующее не компилируется, чего и следовало ожидать ( также на ideone.com):
import java.util.*;
public class TwoListsOfUnknowns2 {
static void doSomethingIllegal(List<?> list1, List<?> list2) {
list1.addAll(list2); // DOES NOT COMPILE!!!
// The method addAll(Collection<? extends capture#1-of ?>)
// in the type List<capture#1-of ?> is not applicable for
// the arguments (List<capture#2-of ?>)
}
}
Пока все хорошо, но вот где вещи начинают становиться очень запутанными ( как видно на ideone.com):
import java.util.*;
public class LOLUnknowns1 {
static void probablyIllegal(List<List<?>> lol, List<?> list) {
lol.add(list); // this compiles!! how come???
}
}
Приведенный выше код компилируется для меня в Eclipse и на sun-jdk-1.6.0.17
в ideone.com, но так ли? Разве не возможно, что у нас есть List<List<Integer>> lol
и List<String> list
, аналогичные две несвязанные ситуации с подстановочными знаками из TwoListsOfUnknowns
?
На самом деле следующая небольшая модификация в этом направлении не компилируется, чего и следовало ожидать ( как видно на ideone.com):
import java.util.*;
public class LOLUnknowns2 {
static void rightfullyIllegal(
List<List<? extends Number>> lol, List<?> list) {
lol.add(list); // DOES NOT COMPILE! As expected!!!
// The method add(List<? extends Number>) in the type
// List<List<? extends Number>> is not applicable for
// the arguments (List<capture#1-of ?>)
}
}
Похоже, что компилятор выполняет свою работу, но затем мы получаем это ( как видно на ideone.com):
import java.util.*;
public class LOLUnknowns3 {
static void probablyIllegalAgain(
List<List<? extends Number>> lol, List<? extends Number> list) {
lol.add(list); // compiles fine!!! how come???
}
}
Опять же, мы можем иметь, например, List<List<Integer>> lol
и List<Float> list
так что это не должно компилироваться, верно?
На самом деле, давайте вернемся к более простому LOLUnknowns1
(два неограниченных подстановочных знака) и попытаться выяснить, можем ли мы на самом деле вызвать probablyIllegal
в любом случае. Давайте сначала попробуем "легкий" случай и выберем один и тот же тип для двух подстановочных знаков ( как видно на ideone.com):
import java.util.*;
public class LOLUnknowns1a {
static void probablyIllegal(List<List<?>> lol, List<?> list) {
lol.add(list); // this compiles!! how come???
}
public static void main(String[] args) {
List<List<String>> lol = null;
List<String> list = null;
probablyIllegal(lol, list); // DOES NOT COMPILE!!
// The method probablyIllegal(List<List<?>>, List<?>)
// in the type LOLUnknowns1a is not applicable for the
// arguments (List<List<String>>, List<String>)
}
}
Это не имеет никакого смысла! Здесь мы даже не пытаемся использовать два разных типа, и он не компилируется! Делая это List<List<Integer>> lol
а также List<String> list
также дает похожую ошибку компиляции! Фактически, из моих экспериментов, единственный способ, которым код компилируется, это если первый аргумент является явным null
тип ( как видно на ideone.com):
import java.util.*;
public class LOLUnknowns1b {
static void probablyIllegal(List<List<?>> lol, List<?> list) {
lol.add(list); // this compiles!! how come???
}
public static void main(String[] args) {
List<String> list = null;
probablyIllegal(null, list); // compiles fine!
// throws NullPointerException at run-time
}
}
Таким образом, вопросы, касающиеся LOLUnknowns1
, LOLUnknowns1a
а также LOLUnknowns1b
:
- Какие типы аргументов делает
probablyIllegal
принимать? - Должен
lol.add(list);
компилировать вообще? Это безопасно? - Это ошибка компилятора или я неправильно понимаю правила преобразования захвата для подстановочных знаков?
Приложение A: Двойной LOL?
В случае, если кому-то интересно, это прекрасно скомпилируется ( как видно на ideone.com):
import java.util.*;
public class DoubleLOL {
static void omg2xLOL(List<List<?>> lol1, List<List<?>> lol2) {
// compiles just fine!!!
lol1.addAll(lol2);
lol2.addAll(lol1);
}
}
Приложение B: Вложенные шаблоны - что они на самом деле означают???
Дальнейшие исследования показывают, что, возможно, несколько подстановочных знаков не имеют ничего общего с проблемой, но скорее всего путаница является вложенным подстановочным знаком.
import java.util.*;
public class IntoTheWild {
public static void main(String[] args) {
List<?> list = new ArrayList<String>(); // compiles fine!
List<List<?>> lol = new ArrayList<List<String>>(); // DOES NOT COMPILE!!!
// Type mismatch: cannot convert from
// ArrayList<List<String>> to List<List<?>>
}
}
Так это выглядит, возможно, List<List<String>>
это не List<List<?>>
, На самом деле, пока любой List<E>
это List<?>
, это не похоже на любой List<List<E>>
это List<List<?>>
( как видно на ideone.com):
import java.util.*;
public class IntoTheWild2 {
static <E> List<?> makeItWild(List<E> list) {
return list; // compiles fine!
}
static <E> List<List<?>> makeItWildLOL(List<List<E>> lol) {
return lol; // DOES NOT COMPILE!!!
// Type mismatch: cannot convert from
// List<List<E>> to List<List<?>>
}
}
Тогда возникает новый вопрос: что же такое List<List<?>>
?
3 ответа
Как показано в Приложении B, это не имеет ничего общего с несколькими подстановочными знаками, а скорее с недопониманием того, что List<List<?>>
действительно значит.
Давайте сначала напомним себе, что это означает, что дженерики Java инвариантны:
-
Integer
этоNumber
-
List<Integer>
НЕ являетсяList<Number>
-
List<Integer>
ЭтоList<? extends Number>
Теперь мы просто применяем тот же аргумент к нашей ситуации с вложенным списком (см. Приложение для более подробной информации):
-
List<String>
является (захватывается)List<?>
-
List<List<String>>
НЕ (захватывается)List<List<?>>
-
List<List<String>>
IS (захватывается)List<? extends List<?>>
При таком понимании все фрагменты в вопросе могут быть объяснены. Путаница возникает в (ложно) верить, что типа, как List<List<?>>
может захватывать такие типы, как List<List<String>>
, List<List<Integer>>
и т. д. Это НЕ правда.
Это List<List<?>>
:
- НЕ является списком, элементы которого являются списками какого-то одного неизвестного типа.
- ... это было бы
List<? extends List<?>>
- ... это было бы
- Вместо этого это список, элементы которого являются списками ЛЮБОГО типа.
обрывки
Вот фрагмент для иллюстрации вышеупомянутых пунктов:
List<List<?>> lolAny = new ArrayList<List<?>>();
lolAny.add(new ArrayList<Integer>());
lolAny.add(new ArrayList<String>());
// lolAny = new ArrayList<List<String>>(); // DOES NOT COMPILE!!
List<? extends List<?>> lolSome;
lolSome = new ArrayList<List<String>>();
lolSome = new ArrayList<List<Integer>>();
Больше фрагментов
Вот еще один пример с ограниченным вложенным подстановочным знаком:
List<List<? extends Number>> lolAnyNum = new ArrayList<List<? extends Number>>();
lolAnyNum.add(new ArrayList<Integer>());
lolAnyNum.add(new ArrayList<Float>());
// lolAnyNum.add(new ArrayList<String>()); // DOES NOT COMPILE!!
// lolAnyNum = new ArrayList<List<Integer>>(); // DOES NOT COMPILE!!
List<? extends List<? extends Number>> lolSomeNum;
lolSomeNum = new ArrayList<List<Integer>>();
lolSomeNum = new ArrayList<List<Float>>();
// lolSomeNum = new ArrayList<List<String>>(); // DOES NOT COMPILE!!
Вернуться к вопросу
Чтобы вернуться к фрагментам в вопросе, следующее ведет себя как ожидалось ( как видно на ideone.com):
public class LOLUnknowns1d {
static void nowDefinitelyIllegal(List<? extends List<?>> lol, List<?> list) {
lol.add(list); // DOES NOT COMPILE!!!
// The method add(capture#1-of ? extends List<?>) in the
// type List<capture#1-of ? extends List<?>> is not
// applicable for the arguments (List<capture#3-of ?>)
}
public static void main(String[] args) {
List<Object> list = null;
List<List<String>> lolString = null;
List<List<Integer>> lolInteger = null;
// these casts are valid
nowDefinitelyIllegal(lolString, list);
nowDefinitelyIllegal(lolInteger, list);
}
}
lol.add(list);
незаконно, потому что мы можем иметь List<List<String>> lol
и List<Object> list
, На самом деле, если мы закомментируем ошибочное утверждение, код скомпилируется, и это именно то, что мы имеем при первом вызове в main
,
Все probablyIllegal
Методы в вопросе, не являются незаконными. Все они совершенно законны и безопасны. В компиляторе нет абсолютно никаких ошибок. Он делает именно то, что должен делать.
Рекомендации
Смежные вопросы
- Любой простой способ объяснить, почему я не могу сделать
List<Animal> animals = new ArrayList<Dog>()
? - Универсальный вложенный шаблон Java не будет компилироваться
Приложение: Правила захвата конвертации
(Это было затронуто в первой редакции ответа; это достойное дополнение к аргументу, инвариантному к типу.)
5.1.10 Преобразование захвата
Пусть G назовет объявление универсального типа с n параметрами формального типа A 1 … A n с соответствующими границами U 1 … U n. Существует преобразование захвата из G
1 … T n > в G1 … S n >, где для 1 <= i <= n:
- Если T i является аргументом подстановочного типа в форме
?
затем …- Если T i является аргументом подстановочного типа в форме
? extends
Тогда я…- Если T i является аргументом подстановочного типа в форме
? super
Тогда я…- В противном случае S i = T i.
Преобразование захвата не применяется рекурсивно.
Этот раздел может сбивать с толку, особенно в отношении нерекурсивного применения преобразования захвата (настоящим CC), но ключ в том, что не все ?
может CC; это зависит от того, где он появляется. В правиле 4 нет рекурсивного применения, но когда применяются правила 2 или 3, то соответствующий B i сам может быть результатом CC.
Давайте рассмотрим несколько простых примеров:
List<?>
может CCList<String>
-
?
может CC по правилу 1
-
List<? extends Number>
может CCList<Integer>
-
?
может CC по правилу 2 - Применяя правило 2, B i просто
Number
-
List<? extends Number>
не могу CCList<String>
-
?
может CC по правилу 2, но ошибка времени компиляции происходит из-за несовместимых типов
-
Теперь давайте попробуем вложить:
List<List<?>>
не могу CCList<List<String>>
- Применяется правило 4, а CC не является рекурсивным, поэтому
?
не могу CC
- Применяется правило 4, а CC не является рекурсивным, поэтому
List<? extends List<?>>
может CCList<List<String>>
- Первый
?
может CC по правилу 2 - При применении правила 2 B i теперь
List<?>
, который может УКList<String>
- И то и другое
?
может CC
- Первый
List<? extends List<? extends Number>>
может CCList<List<Integer>>
- Первый
?
может CC по правилу 2 - При применении правила 2 B i теперь
List<? extends Number>
, который может УКList<Integer>
- И то и другое
?
может CC
- Первый
List<? extends List<? extends Number>>
не могу CCList<List<Integer>>
- Первый
?
может CC по правилу 2 - При применении правила 2 B i теперь
List<? extends Number>
, который может CC, но дает ошибку времени компиляции применительно кList<Integer>
- И то и другое
?
может CC
- Первый
Для дальнейшей иллюстрации, почему некоторые ?
может CC и другие не могут, рассмотрим следующее правило: вы НЕ можете напрямую создавать экземпляры подстановочного типа. То есть следующее дает ошибку времени компиляции:
// WildSnippet1
new HashMap<?,?>(); // DOES NOT COMPILE!!!
new HashMap<List<?>, ?>(); // DOES NOT COMPILE!!!
new HashMap<?, Set<?>>(); // DOES NOT COMPILE!!!
Тем не менее, следующие компиляции просто отлично:
// WildSnippet2
new HashMap<List<?>,Set<?>>(); // compiles fine!
new HashMap<Map<?,?>, Map<?,Map<?,?>>>(); // compiles fine!
Причина WildSnippet2
компилируется потому, что, как объяснено выше, ни один из ?
может CC. В WildSnippet1
Либо K
или V
(или оба) из HashMap<K,V>
может CC, который делает прямую реализацию через new
незаконным.
Никакой аргумент с дженериками не должен быть принят. В случае
LOLUnknowns1b
null
принимается так, как если бы первый аргумент был напечатан какList
, Например, это компилируется:List lol = null; List<String> list = null; probablyIllegal(lol, list);
по моему мнению
lol.add(list);
не должен даже компилироваться, но какlol.add()
нужен аргумент типаList<?>
и как список вписывается вList<?>
оно работает.
Странный пример, который заставляет меня думать об этой теории:static void probablyIllegalAgain(List<List<? extends Number>> lol, List<? extends Integer> list) { lol.add(list); // compiles fine!!! how come??? }
lol.add()
нужен аргумент типаList<? extends Number>
и список набирается какList<? extends Integer>
, он подходит. Он не будет работать, если он не совпадает. То же самое для двойного LOL и других вложенных подстановочных знаков, если первый захват совпадает со вторым, все в порядке (и не должно быть).Опять же, я не уверен, но это действительно похоже на ошибку.
Я рад, что не единственный, кто использует
lol
переменные все время.
Ресурсы:
http://www.angelikalanger.com, FAQ о дженериках
РЕДАКТИРОВАТЬ:
- Добавлен комментарий о Double Lol
- И вложенные подстановочные знаки.
Не эксперт, но я думаю, что могу это понять.
давайте изменим ваш пример на что-то эквивалентное, но с более отличительными типами:
static void probablyIllegal(List<Class<?>> x, Class<?> y) {
x.add(y); // this compiles!! how come???
}
давайте изменим список на [], чтобы сделать его более ярким:
static void probablyIllegal(Class<?>[] x, Class<?> y) {
x.add(y); // this compiles!! how come???
}
теперь x не является массивом некоторого типа класса. это массив любого типа класса. он может содержать Class<String>
и Class<Int>
, это нельзя выразить обычным параметром типа:
static<T> void probablyIllegal(Class<T>[] x //homogeneous! not the same!
Class<?>
это супер тип Class<T>
для любого T
, Если мы думаем, что тип - это набор объектов, установите Class<?>
это объединение всех наборов Class<T>
для всех T
, (это включает в себя это? Я не знаю...)