Причины этого основаны на том, как Java реализует дженерики.Лучший способ объяснить это - сначала использовать массивы.
Пример массива
С массивами вы можете сделать это:
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Но что произойдет, если вы попытаетесь это сделать?
Number[0] = 3.14; //attempt of heap pollution
Эта последняя строка скомпилируется просто отлично, но если вы запустите этот код, вы можете получить ArrayStoreException
.
Это означает, что вы можете обмануть компилятор, но вы не можете обмануть систему типов времени выполнения.И это так, потому что массивы - это то, что мы называем типами reifiable .Это означает, что во время выполнения Java знает, что этот массив был фактически создан как массив целых чисел, к которым просто случается обращение через ссылку типа Number[]
.
Итак, как вы можете видеть, одна вещь - этоРеальный тип объекта, другая вещь - это тип ссылки, которую вы используете для доступа к ней, верно?
Проблема с обобщением Java
ТеперьПроблема с универсальными типами Java заключается в том, что информация о типах отбрасывается компилятором и недоступна во время выполнения.Этот процесс называется тип стирания .Есть веская причина для реализации таких обобщений в Java, но это длинная история, и она связана с бинарной совместимостью с уже существующим кодом.
Но важным моментом здесь является то, что, поскольку во время выполнениянет информации о типе, нет способа гарантировать, что мы не совершаем загрязнение кучи.
Например,
List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts;
myNums.add(3.14); //heap polution
Если компилятор Java не мешает вам делать это при компиляцииВ то же время система типов времени выполнения не может вас остановить, потому что во время выполнения невозможно определить, что этот список должен быть только списком целых чисел.Среда выполнения Java позволила бы вам поместить в этот список все, что вы хотите, когда он должен содержать только целые числа, потому что, когда он был создан, он был объявлен как список целых чисел.
Таким образом, разработчики Java сделалиуверен, что вы не можете обмануть компилятор.Если вы не можете обмануть компилятор (как мы можем это сделать с массивами), вы также не можете обмануть систему типов времени выполнения.
Таким образом, мы говорим, что универсальные типы non-reifiable .
Очевидно, это также будет препятствовать полиморфизму.Решение состоит в том, чтобы научиться использовать две мощные функции обобщений Java, известные как ковариация и контравариантность.
Ковариация
С помощью ковариации вы можете читать элементы из структуры, но выне могу ничего написать в него.Все это действительные декларации.
List<? extends Number> myNums = new ArrayList<Integer>();
List<? extends Number> myNums = new ArrayList<Float>()
List<? extends Number> myNums = new ArrayList<Double>()
И вы можете прочитать из myNums
:
Number n = myNums.get(0);
Поскольку вы можете быть уверены, что, что бы ни содержал фактический список, его можно преобразовать вчисло (ведь все, что расширяет число, является числом, верно?)
Однако вам не разрешено помещать что-либо в ковариантную структуру.
myNumst.add(45L);
Это не разрешенопотому что Java не может гарантировать, что является реальным типом реального объекта.Это может быть что угодно, что расширяет Number, но компилятор не может быть уверен.Таким образом, вы можете читать, но не писать.
Контравариантность
С помощью контравариантности вы можете делать противоположное.Вы можете поместить вещи в общую структуру, но вы не можете читать из нее.
List<Object> myObjs = new List<Object();
myObjs.add("Luke");
myObjs.add("Obi-wan");
List<? super Number> myNums = myObjs;
myNums.add(10);
myNums.add(3.14);
В этом случае фактическая природа объекта - это Список объектов, и с помощью контравариантности вы можете поместить числав это, в основном, потому что числа имеют Объект в качестве общего предка.Таким образом, все числа являются объектами, и поэтому это действительно.
Однако вы не можете безопасно читать что-либо из этой контравариантной структуры, предполагая, что вы получите число.
Number myNum = myNums.get(0); //compiler-error
Как видите, если бы компилятор позволил вам написать эту строку, вы получите ClassCastException во время выполнения.
Принцип Get / Put
Таким образом, используйте ковариацию, когда вы намерены извлекать универсальные значения только из структуры, используйте контравариантность, когда вы только намереваетесь поместить универсальные значения в структуру, и используйте точный универсальный тип, когда вы собираетесь сделать оба.
Лучший пример, который у меня есть, - это копирование любых чисел из одного списка в другой.
public static void copy(List<? extends Number> source, List<? super Number> destiny) {
for(Number number : source) {
destiny.add(number);
}
}
Благодаря силам ковариации и контравариантности это работает для случая, подобного этому:
List<Integer> myInts = asList(1,2,3,4);
List<Integer> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();
copy(myInts, myObjs);
copy(myDoubles, myObjs);