Назначение многоуровневых подстановочных знаков
Простой класс:
class Pair<K,V> {
}
И несколько назначений:
Collection<Pair<String,Long>> c1 = new ArrayList<Pair<String,Long>>();
Collection<Pair<String,Long>> c2 = c1; // ok
Collection<Pair<String,?>> c3 = c1; // this does not compile
Collection<? extends Pair<String,?>> c4 = c1; // ok
почему пуля номер три не компилируется, а четвертая совершенно законна?
Ошибка компилятора:
Type mismatch: cannot convert from Collection<Pair<String,Long>> to Collection<Pair<String,?>>
Ответы
Ответ 1
Я попытаюсь объяснить генераторы Java, используя два простых правила. Этих правил достаточно, чтобы ответить на ваш вопрос и в основном достаточно, чтобы запомнить практически в любом случае:
- Два общих типа
X<A>
и X<B>
никогда не назначаются, если A = B
. I.e., дженерики по умолчанию инвариантны.
- Подстановочные знаки допускают назначение
X<A>
:
- to
X<?>
- to
X<? extends T>
iff A
присваивается T
(рекурсивно применяйте правила к A
и T
)
- to
X<? super T>
iff T
можно присваивать A
(рекурсивно применять правила к T
и A
)
Случай c3 = c1
В вашем примере вы пытаетесь присвоить Collection<Pair<String,Long>>
Collection<Pair<String,?>>
. То есть в вашем случае A = Pair<String,Long>
и B = Pair<String,?>
. Поскольку эти типы не равны, они не могут быть назначены; они нарушают правило 1.
Вопрос в том, почему эта подсказка не помогает? Ответ прост:
Правило 2 НЕ транзитивно. I.e., X<X<A>>
не может быть оценено X<X<?>>
, на внешнем уровне должен быть подстановочный знак; в противном случае правило 2 не применяется к самому внешнему уровню.
Случай c4 = c1
Здесь у вас есть подстановочный знак внешнего типа. Поскольку он находится во внешнем типе, правило 2 запускается: A = Pair<String,?>
назначается B = ? extends Pair<String,Long>
(опять же из-за правила 2). Поэтому это законно.
Общий подход
Вот как вы можете проверить любой сложный общий тип: просто проверьте каждый общий уровень по уровню, используя два правила. Начните с самого внешнего уровня. Когда уровень нарушает правила, вы знаете, что назначение является незаконным; если все уровни соответствуют правилам, то назначение является законным. Давайте снова рассмотрим ваши типы:
X = Collection<Pair<String,Long>>
Y = Collection<Pair<String,?>>
Z = Collection<? extends Pair<String,?>>
Является ли X назначаемым Y?
// Outermost level:
A = Pair<String,Long>, B = Pair<String,?>
=> B is no wildcard and A != B (Rule 1), so this is illegal!
Является ли X назначаемым Z?
// Outermost level:
A = Pair<String,Long>, B = ? extends Pair<String,?>
=> We got a wildcard, so Rule 2 states this is legal if the inner level is legal
// Inner level: (we have to check both parameters)
A = String, B = String => Equal, Rule 1 applies, fine!
A = Long, B = ? => B is wildcard, Rule 2 applies, fine!
Простое правило для запоминания
Каждый уровень общего гнездования должен быть полностью идентичным (A=B
) или B
должен содержать подстановочный знак на этом уровне.
Ответ 2
Прежде всего, упростите код, удалив дополнительный параметр типа:
Collection<List<Long>> c1 = new ArrayList<List<Long>>();
Collection<List<Long>> c2 = c1; // ok
Collection<List<?>> c3 = c1; // this does not compile
Collection<? extends List<?>> c4 = c1; // ok
Мы знаем, что List<? extends T>
по существу означает "a List
вы можете получить T
from", а List<?>
- это то же самое, что и List<? extends Object>
.
Итак, типы перечисленных выше переменных можно объяснить следующим образом:
- c3: "коллекция
List
, которая позволяет вам получить Object
от них
- c4: "коллекция, которая позволяет вам получить
List
, которые позволят вам получить Object
от них
В частности, это объяснение подразумевает следующее:
// The following line compiles,
// because `ArrayList<String>` is a `List` you can get `Object`s from
c3.add(new ArrayList<String>());
// The following line does not compile,
// because type of c4 doesn't allow you to put anything into it
c4.add(new ArrayList<String>());
Теперь это разрешено c3 = c1
, вы можете видеть, что c3.add(new ArrayList<String>())
нарушит безопасность типа c1
:
Collection<List<Long>> c1 = new ArrayList<List<Long>>();
Collection<List<?>> c3 = c1;
c3.add(Arrays.asList("foo"));
for (List<Long> l: c1) {
for (Long value: l) {
// Oops, value is not a Long!
}
}
Ответ 3
Ключевая вещь, которую нужно запомнить, - это то, что вложенные подстановочные знаки не захватываются.
Это означает, что "нормальное" поведение, которое вы ожидаете от подстановочных знаков верхнего уровня (т.е. подстановочные знаки для одного конкретного), не относится к вложенным подстановочным символам. Вместо этого вложенные подстановочные знаки обозначают любой тип.
Например, возьмите это объявление:
List<?> l;
Это означает, что l
является List
одного конкретного типа. Легко.
Но как насчет этого?
Collection<List<?>> c;
Это не Collection
из List
одного конкретного типа. Это Collection
of List
s, каждый из которых является одним конкретным типом.
Например, вы ожидали чего-то подобного:
Collection<List<?>> c = new ArrayList<List<Long>>(); // Not valid, but pretend it is
c.add(new ArrayList<Long>()); // Valid
c.add(new ArrayList<Integer>()); // Invalid, because c is a Collection of Lists of Long
Но рассмотрим следующее:
List<?> l = new ArrayList<String>();
c.add(l); // Should this compile?
Тип l
точно соответствует параметру type для c
, правильно? Поэтому не стоит добавлять l
в c
, хотя l
не является List<Long>
?
Также рассмотрим следующее:
c.iterator().next(); // Assume there is an element to return
Какой тип должен возвращаться? iterator()
возвращает Iterator<E>
, а next()
возвращает E
, что означает... c.iterator().next()
возвращает a List<?>
. Который не был List<Long>
, который вы ожидали. Почему это?
Так как вложенные подстановочные знаки не захватываются. И это ключевое различие здесь. Подстановочный знак в List<?>
не фиксирует один тип "общий". Он фиксирует один тип для каждого из элементов в Collection
.
Таким образом, это вполне допустимый код:
Collection<List<?>> odd = new ArrayList<List<?>>();
odd.add(new ArrayList<String>());
odd.add(new ArrayList<Long>());
List<?> l = odd.iterator().next();
// returns the ArrayList<String>, but because odd is parameterized with
// List<?> we can technically end up with a list of anything
Помня об этом, давайте посмотрим на ваши примеры.
Collection<Pair<String,Long>> c1 = new ArrayList<Pair<String,Long>>();
Collection<Pair<String,Long>> c2 = c1;
Это интуитивно нормально. Типы точно совпадают, поэтому c1
присваивается c2
.
Collection<Pair<String,Long>> c1 = new ArrayList<Pair<String,Long>>();
Collection<Pair<String,?>> c3 = c1;
Теперь, оглянитесь назад. A Collection<Pair<String,?>>
не является Collection
of Pair
of String
и одним неизвестным типом. Это a Collection
of Pair
s, каждый из которых представляет собой пару a String
и некоторый неизвестный тип, который может или не может быть того же типа, что и другая пара в коллекции. Так что это действительно:
// Assume an appropriate object was assigned to c3
Pair<String, ?> p1 = new Pair<String, String>("Hello", "World");
Pair<String, ?> p2 = new Pair<String, List<String>>("Lorem", new ArrayList<>());
Pair<String, ?> p3 = new Pair<String, Map<String, Integer>>("Ispum", new HashMap<>());
c3.add(p1);
c3.add(p2);
c3.add(p3);
И поскольку это допустимо для c3
, но не должно быть действительным для c1
, назначение c1
на c3
не разрешено, поскольку это позволит вам помещать материал в ArrayList<Pair<String, Long>>
это не Pair<String, Long>
.
Collection<Pair<String,Long>> c1 = new ArrayList<Pair<String,Long>>();
Collection<? extends Pair<String,?>> c4 = c1;
Теперь это немного сложнее. Верхний уровень захватывает один конкретный тип, который расширяет Pair<String, ?>
. Поскольку подстановочные знаки являются супертипами определенных типов (например, List<?>
является супертипом List<Integer>
), Pair<String, Long>
может быть захвачен ? extends Pair<String, ?>
, поскольку первый расширяет его. Таким образом, поскольку Pair<String, Long>
совместим с назначением с ? extends Pair<String, ?>
, присваивание действительно.
Как вы можете сказать из множества ответов здесь, существует несколько способов объяснить поведение вложенных подстановочных знаков. Я собирался немного больше интуитивного объяснения, которое, я надеюсь, я достиг.
Ответ 4
Одно из преимуществ Java-дженериков - более сильные проверки типов во время компиляции. Так что все, что объявлено в дженериках, должно точно совпадать. Чтобы сделать это простым ответом, я бы использовал несколько примеров.
Чтобы инициализировать список номеров, вы могли бы сделать.
List<Number> numbers = new ArrayList<Number>();
Это технически означает, что список "числа" может хранить любой объект, который является числом или подклассом числа. Но мы не можем инициализировать этот список с подтипами. Все, что указано в общих тегах <>
, должно буквально соответствовать назначению. (Java 7 предоставляет вывод типа, хотя)
List<Number> intNumbers = new ArrayList<Interger>(); // Compile Error
List<Number> doubleNumbers = new ArrayList<Double>(); // Compile Error
List<List<String>> list = new ArrayList<ArrayList<String>>(); // Compile Error
Несмотря на то, что Integer и Double являются подклассами Number, генерики предотвращают эту инициализацию. Общий аргумент, указанный внутри <>
, должен точно совпадать.
Теперь, если назначение тесно связано, как список номеров хранит объекты подкласса? Ответ - методы add(), addlAll().. etc
принимают E
или все, что расширяет E
, где E
- это общий тип, который мы дали. Поэтому в случае списка "числа" E это Number
, поэтому справедливы следующие утверждения.
List<Integer> intNumbers = new ArrayList<Integer>();
List<Number> numbers = new ArrayList<Number>();
numbers.add(new Integer(1));
numbers.add(new Double(1.0));
numbers.addAll(intNumbers);
Снова одно исключение - это подстановочные знаки. Подстановочный знак используется для принятия любых аргументов. Таким образом, действуют следующие статусы.
List<?> numbers = new ArrayList<Number>();
Map<?, ?> unKnownMap = new HashMap<String, String>();
// OR
List<Integer> intNumbers = new ArrayList<Integer>();
List<?> numbers = intNumbers;
Аналогично
Pair<String,?> p = new Pair<String, Long>(); // Is valid.
Но теперь мы не можем добавить ничего в этот список, поскольку этот элемент должен расширять ?
, и это неизвестный тип, а единственным допустимым элементом является null
, который является членом каждого типа. Java не выводит это из назначения. Таким образом, следующий результат при ошибке
List<?> numbers = new ArrayList<Number>(); // Works fine.
numbers.add(new Integer(1)); // Compile Error
numbers.add(new Double(1.0)); // Compile Error
Кроме того, вывод подстановочного символа происходит только на одном уровне. Любая вложенность должна снова совпадать точно так же, как и обычные дженерики. Итак,
List<List<?>> list = new ArrayList<List<String>>(); // Compile error because nested List<String> doesn not exactly match List<?>
List<List<?>> list = new ArrayList<List<?>>(); // OK - Valid
List<List<Integer>> intList = new ArrayList<List<Integer>>();
List<List<?>> numbers = intList; // Compile error because nested List<Integer> doesn not exactly match List<?>
Итак, это ваш случай
Collection<Pair<String,Long>> c1 = new ArrayList<Pair<String,Long>>();
Collection<Pair<String,Long>> c2 = c1; // ok
Collection<Pair<String,?>> c3 = c1; // Compile error because nested Pair<String,?> doesn not exactly match Pair<String,Long>
Итак, вы можете сделать что-то вроде
Collection<Pair<String,?>> c3 = new ArrayList<Pair<String,?>>(c2);
//OR
Collection<Pair<String,?>> c3 = new ArrayList<Pair<String,?>>();
c3.addAll(c2);
Случай 4: Когда вы говорите List<? extends List<?>>
его снова первый уровень. Это означает, что он проверяет, распространяется ли элемент под вопросом List<?>
.
List<? extends List<?>> list = new ArrayList<List<String>>(); // Works similar to List<?> l = new ArrayList<String>();
List<? extends List<List<?>>> numbers = new ArrayList<List<List<String>>>(); // Compile error - Nested level similar to List<List<String>> lst = new ArrayList<List<String>>();
Итак, Collection<Pair<String,?>> c3 = new ArrayList<Pair<String, Long>>();
похож на Pair<String,?> p = new Pair<String, Long>();