Подтвердить что ты не робот

Неравномерности с (?) Подстановочным знаком типа

Я считаю, что тип ? в generics - это определенный неизвестный тип. Это означает, что объявление, скажем, списка такого типа, помешает нам добавить в него какой-либо объект.

List<?> unknownList;
unknownList.add(new Object()); // This is an error.

Компилятор выдает ошибку, как ожидалось.

Но когда неизвестный тип является генератором второго уровня, компилятор, похоже, не заботится.

class First<T> {}

List<First<?>> firstUnknownList;

// All these three work fine for some reason.
firstUnknownList.add(new First<>());
firstUnknownList.add(new First<Integer>());
firstUnknownList.add(new First<String>());

Я думал, вероятно, компилятор вообще не заботится об общем параметре на втором уровне, но это не так,

List<First<Integer>> firstIntegerList;
firstIntegerList.add(new First<String>()); // This gives a compiler error as expected.

Итак, почему компилятор позволяет нам добавлять какой-либо элемент, когда во втором примере допустим только неизвестный элемент (и, следовательно, ничего)?

Примечание. Компилятор Java 1.8

4b9b3361

Ответ 1

Вы можете добавить что-нибудь к List<T>, которое вы можете сохранить в ссылке типа T:

T item = ...
List<T> list = new ArrayList<>();
list.add(item);

First<?> является супертипом First<T>; поэтому вы можете сохранить ссылку на First<T> в переменной типа First<?>:

First<?> first = new First<String>();

Итак, подставив T для First<?> выше:

First<?> item = new First<String>();
List<First<?>> list = new ArrayList<>();
list.add(item);

Все, что происходит в примере OP, состоит в том, что временная переменная item опущена:

firstUnknownList.add(new First<String>());

Однако, если вы сделаете это с помощью примера firstIntegerList:

First<Integer> item = new First<String>(); // Compiler error.
List<First<Integer>> list = new ArrayList<>();
list.add(item);

понятно, почему это не допустимо: вы не можете выполнить назначение item.


Также можно увидеть, что вы не можете делать ничего небезопасного с содержимым этого списка.

Если вы добавите несколько методов в интерфейс:

interface First<T> {
  T producer();
  void consumer(T in);
}

Теперь рассмотрим, что вы можете сделать с элементами, добавленными в список:

for (First<?> first : firstUnknownList) {
  // OK at compile time; OK at runtime unless the method throws an exception.
  Object obj = first.producer();

  // OK at compile time; may fail at runtime if null is not an acceptable parameter.
  first.consumer(null);

  // Compiler error - you can't have a reference to a ?.
  first.consumer(/* some maybe non-null value */);
}

поэтому на самом деле нет ничего, что вы действительно можете сделать с элементами этого списка, которые нарушали бы безопасность типов (при условии, что вы не будете делать ничего намеренного, чтобы нарушать его, например, используя необработанные типы). Вы можете продемонстрировать, что общие методы производителя/потребителя аналогичным образом защищены или запрещены компилятором.

Поэтому нет причин не позволять вам это делать.

Ответ 2

Я изменю First интерфейс на интерфейс Box

Box<?> uknownBox Серый ящик Box<?> uknownBox с чем-то в нем

Box<Apple> appleBox Ящик с яблоком

List<Box<Apple>> appleBoxList много коробок с яблоками

List<Box<?>> uknownBoxList много неизвестных серых ящиков

appleBoxList.add(new Box<Orange>()) - не могу добавить коробку с апельсинами в список яблочных коробок

unknownBoxList.add(new Box<?>()) - мы не знаем, что в этих серых полях, добавление еще одного неизвестного серого прямоугольника ничего не меняет

unknownBoxList.add(new Box<Orange>()) - same rules when you add specific boxes
unknownBoxList.add(new Box<Apple>()) - since you are not allowed to 'open' them

unknownBoxList = appleBoxList это не компилируется, чтобы предотвратить добавление серых (возможно, не яблочных) ящиков в список ящиков яблок. из-за того, что предыдущая операция является законной.

Ответ 3

Это все о подтипах/супертипах.

List<?> - это список, содержащий элементы неизвестного (но конкретного) типа. Вы никогда не знаете, какой тип содержится в этом списке. Поэтому вы не можете добавлять к нему объекты, потому что они могут быть неправильного типа:

List<Integer> ints = new ArrayList<Integer>();
List<?> unknowns = ints;

// It this worked, the list would contain a String....
unknowns.add("String"); 

// ... and this would crash with some ClassCastException
Integer i = ints.get(0);

Также может быть ясно, что вы можете сделать

List<Number> numbers = null;
Integer integer = null;
numbers.add(integer); 

Это работает, потому что Number является истинным супертипом Integer. Он не нарушает безопасность типа, чтобы добавить в список объект более определенного типа.


Ключевой момент для второго примера:

First<?> является супертипом каждого First<T>

Вы всегда можете найти что-то вроде

First<Integer> fInt = null;
First<Integer> fString = null;

First<?> f = null;
f = fInt; // works
f = fString; // works

Итак, причина, по которой вы можете добавить First<String> в List<First<?>>, такая же, как и почему вы можете добавить Integer в List<Number>: элемент, который вы хотите добавить, имеет значение true подтипа элементов, ожидаемых в списке.

Ответ 4

Я считаю, что этот тип? в generics - это определенный неизвестный тип.

Это немного неточно. Да, подстановочный знак обозначает неизвестный тип, но может быть разным для разных типов:

List<?> list = new ArrayList<String>();
list = new ArrayList<Integer>();

Единственный инвариант состоит в том, что выражение, тип которого содержит подстановочный знак, всегда будет давать значение, тип которого соответствует этому шаблону. Поскольку каждое значение имеет тип, который не является только подстановочным знаком, можно сказать, что подстановочный знак обозначает (более) "конкретный" тип в любое время.