Назначение многоуровневых подстановочных знаков

Простой класс:

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>();