Тип Erasure в ArrayList > универсальный c звонок - PullRequest
1 голос
/ 05 марта 2020

Может кто-нибудь объяснить, почему следующий код не компилируется:

ArrayList<List<?>> arrayList = new ArrayList<List<String>>();

Почему вышеуказанный код недействителен, но этот работает нормально:

ArrayList<?> items = new ArrayList<List<String>>();

1 Ответ

2 голосов
/ 05 марта 2020

Давайте начнем с простых списков. Integer является подклассом Number, поэтому вы можете назначить Integer на Number:

Integer i = 42;
Number n = i;  // Ok

Теперь давайте получим List<Number>. Разве мы не можем поставить туда List<Integer>?

List<Number> numbers = new ArrayList<Integer>(); // Fails

Нет, не получится. Почему? Поскольку генерики в Java являются инвариантными, это означает, что для любых двух различных типов T1 и T2 ни List<T1> не является подтипом List<T2>, ни List<T2> не является подтипом List<T1> независимо от отношений между T1 и T2. Хорошо, это формально объясняет, почему вышеприведенное присваивание недействительно, но что за логика c стоит за этим?

List<Number> должен иметь возможность хранить любое число, это означает, что если у нас есть объявление

List<Number> numbers;

тогда мы сможем сделать number.add(Integer(42)), number.add(Double(Math.PI)) и любые другие number.add(subClassOfNumber). Однако если указанное выше назначение

List<Number> numbers = new ArrayList<Integer>();  // ?

будет действительным, то numbers теперь будет содержать список, который может хранить только целые числа, поэтому numbers.add(anyNumberButInteger) потерпел бы неудачу, что нарушает договор о List<Number>.

Можно создать список, который будет содержать экземпляры некоторого подкласса Number:

List<? extends Number> list = Arrays.asList(5, 6, 7, 8);  // Ok

Этот список можно прочитать без проблем:

System.out.println(list.get(2));   // 7

Однако вы не можете поместить туда ничего, кроме нуля, потому что невозможно знать точный тип элементов списка во время компиляции и, следовательно, невозможно выполнить проверку на безопасность:

List<? extends Number> list = ThreadLocalRandom.current().nextBoolean() ? 
                                   new ArrayList<Integer>() : 
                                   new ArrayList<Double>();
list.add(null);      // Ok
list.add(Double.valueOf(3.1415));    //  Fails, because we don't know if Double(3.1415) is actually compatible with elements of the list

Хорошо, теперь к вашему примеру:

List<List<?>> listOfLists = new ArrayList<List<String>>();

Чтобы это задание работало, вам нужно иметь слева List<? extends List<String>>, но, поскольку Java generi c является инвариантом, единственный тип, который расширяет List<String>, это сам List<String>, поэтому единственной допустимой комбинацией является

List<List<String>> listOfListsOfStrings = new ArrayList<List<String>>();

. Можно создать переменную, представляющую список, которая состоит из некоторых списков, используя raw типы:

List<? extends List> listOfLists = new ArrayList<List<String>>(); // Compiles, but loses generics. DON'T DO THIS!
List<String> listOfStrings = listOfLists.get(0);                  // Compiles, even would work as expected assuming we populated listOfLists correctly
List<Integer> listOfIntegers = listOfLists.get(0);                // Compiles, but does not work as expected (fail at runtime when accessing elements)

TLDR: единственный универсальный c класс, который супер / расширяется List<String> равен List<String>, поэтому List<some class which extends List<String>> автоматически означает List<List<String>>.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...