Множественные подстановочные знаки для общих методов делают 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>
и a 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
и a 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???
}
}
Опять же, мы можем иметь, например, a List<List<Integer>> lol
и a 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. ком):
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<?>>
?
Ответы
Ответ 1
Как указано в Приложении B, это не имеет никакого отношения к нескольким подстановочным знакам, а скорее к неправильному пониманию того, что действительно означает List<List<?>>
.
Позвольте сначала напомнить себе, что означает, что Java-дженерики являются инвариантными:
- An
Integer
является Number
- A
List<Integer>
НЕ является List<Number>
- A
List<Integer>
IS a List<? extends Number>
Теперь мы просто применяем тот же аргумент к нашей ситуации с вложенным списком (подробнее см. приложение)
:
- A
List<String>
(может быть захвачен) a List<?>
- A
List<List<String>>
НЕ (доступен для захвата) a List<List<?>>
- A
List<List<String>>
IS (доступен для захвата) a List<? extends List<?>>
С учетом этого можно объяснить все фрагменты в вопросе. Путаница возникает (ложно), полагая, что тип типа List<List<?>>
может захватывать типы типа List<List<String>>
, List<List<Integer>>
и т.д. Это НЕ верно.
То есть a 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
и a List<Object> list
. Фактически, если мы прокомментируем выражение о нарушении, код компилируется и что именно то, что мы имеем с первым вызовом в main
.
Все методы probablyIllegal
в вопросе не являются незаконными. Все они совершенно законные и типичные. В компиляторе абсолютно нет ошибок. Он делает именно то, что он должен делать.
Ссылки
Связанные вопросы
Приложение: Правила преобразования захвата
(Это было поднято в первой ревизии ответа, это достойное дополнение к инвариантному аргументу типа.)
Пусть G называет объявление общего типа с n формальными параметрами A 1... A n с соответствующими границами U 1... U псуб > . Существует преобразование захвата из G < T 1... T n > к G < S 1... S n > , где, для 1 <= я <= n:
- Если T i является аргументом типа подстановки формы
?
, то... - Если T i является аргументом типа подстановки формы
? extends
B i, тогда... - Если T i является аргументом типа подстановки формы
? super
B i, тогда... - В противном случае S i= T i.
Преобразование захвата не применяется рекурсивно.
Этот раздел может ввести в заблуждение, особенно в отношении нерекурсивного применения преобразования захвата (здесь CC), но ключ заключается в том, что не все ?
могут CC; это зависит от того, где он появляется. В правиле 4 нет рекурсивного приложения, но когда применяются правила 2 или 3, соответствующий B i сам может быть результатом CC.
Проделайте несколько простых примеров:
-
List<?>
может CC List<String>
-
List<? extends Number>
может CC List<Integer>
-
?
может CC по правилу 2
- При применении правила 2 B i просто
Number
-
List<? extends Number>
не может СС List<String>
-
?
может CC по правилу 2, но ошибка времени компиляции возникает из-за несовместимых типов
Теперь попробуйте некоторое вложение:
-
List<List<?>>
не может СС List<List<String>>
- Правило 4 применяется, и CC не является рекурсивным, поэтому
?
НЕ может СС
-
List<? extends List<?>>
может CC List<List<String>>
- Первая
?
может CC по правилу 2
- При применении правила 2 B i теперь имеет значение
List<?>
, которое может CC List<String>
- Оба
?
могут CC
-
List<? extends List<? extends Number>>
может CC List<List<Integer>>
- Первая
?
может CC по правилу 2
- При применении правила 2 B i теперь является
List<? extends Number>
, который может CC List<Integer>
- Оба
?
могут CC
-
List<? extends List<? extends Number>>
не может СС List<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>
могут СС, что делает прямое инстанцирование через new
незаконным.
Ответ 2
-
Нет аргументов с дженериками. В случае LOLUnknowns1b
null
принимается так, как будто первый аргумент был введен как List
. Например, это компилируется:
List lol = null;
List<String> list = null;
probablyIllegal(lol, list);
-
IMHO 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
- И вложенные подстановочные знаки.
Ответ 3
не эксперт, но я думаю, что могу это понять.
измените свой пример на нечто эквивалентное, но с более отличительными типами:
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 является not массивом некоторого типа класса. это массив любого типа класса. он может содержать Class<String>
и a Class<Int>
. это не может быть выражено с помощью обычного параметра типа:
static<T> void probablyIllegal(Class<T>[] x //homogeneous! not the same!
Class<?>
является супер-типом Class<T>
для любого T
. Если мы считаем, что тип представляет собой набор объектов, set Class<?>
является объединением всех наборов Class<T>
для всех T
. (не включает ли он его, я не знаю...)