Причины этого основаны на том, как Java реализует дженерики.
Пример массива
С массивами вы можете сделать это (массивы ковариантны)
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Но что произойдет, если вы попытаетесь это сделать?
myNumber[0] = 3.14; //attempt of heap pollution
Эта последняя строка прекрасно скомпилируется, но если вы запустите этот код, вы можете получить ArrayStoreException
. Потому что вы пытаетесь поместить двойное число в массив целых чисел (независимо от того, к чему вы обращаетесь через ссылку на число).
Это означает, что вы можете обмануть компилятор, но вы не можете обмануть систему типов времени выполнения. И это так, потому что массивы - это то, что мы называем типами reifiable . Это означает, что во время выполнения Java знает, что этот массив был фактически создан как массив целых чисел, к которым просто случается обращение через ссылку типа Number[]
.
Итак, как вы можете видеть, одна вещь - это фактический тип объекта, а другая вещь - это тип ссылки, которую вы используете для доступа к ней, верно?
Проблема с Java Generics
Теперь проблема с универсальными типами Java заключается в том, что информация о типах отбрасывается компилятором и недоступна во время выполнения. Этот процесс называется стиранием типа . Есть веская причина для реализации таких обобщений в Java, но это длинная история, и она связана, помимо прочего, с бинарной совместимостью с уже существующим кодом (см. Как мы получили обобщенные образцы, которые у нас есть) ).
Но важным моментом здесь является то, что, поскольку во время выполнения нет информации о типе, нет способа гарантировать, что мы не допустим загрязнения кучи.
Например,
List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap pollution
Если компилятор Java не мешает вам сделать это, система типов времени выполнения также не может остановить вас, потому что во время выполнения нет никакого способа определить, что этот список должен быть списком только целых чисел. Среда выполнения Java позволит вам поместить все, что вы хотите в этот список, когда он должен содержать только целые числа, потому что, когда он был создан, он был объявлен как список целых чисел.
Таким образом, разработчики Java позаботились о том, чтобы вы не могли обмануть компилятор. Если вы не можете обмануть компилятор (как мы можем сделать с массивами), вы также не можете обмануть систему типов времени выполнения.
Таким образом, мы говорим, что универсальные типы не подлежат переопределению .
Очевидно, это помешало бы полиморфизму. Рассмотрим следующий пример:
static long sum(Number[] numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
Теперь вы можете использовать это так:
Integer[] myInts = {1,2,3,4,5};
Long[] myLongs = {1L, 2L, 3L, 4L, 5L};
Double[] myDoubles = {1.0, 2.0, 3.0, 4.0, 5.0};
System.out.println(sum(myInts));
System.out.println(sum(myLongs));
System.out.println(sum(myDoubles));
Но если вы попытаетесь реализовать тот же код с универсальными коллекциями, у вас ничего не получится:
static long sum(List<Number> numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
Вы получите ошибку компилятора, если попытаетесь ...
List<Integer> myInts = asList(1,2,3,4,5);
List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0);
System.out.println(sum(myInts)); //compiler error
System.out.println(sum(myLongs)); //compiler error
System.out.println(sum(myDoubles)); //compiler error
Решение состоит в том, чтобы научиться использовать две мощные функции обобщений 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); //compiler error
Это было бы недопустимо, потому что 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 во время выполнения.
Принцип получения / сдачи
Таким образом, используйте ковариацию, когда вы намереваетесь извлекать общие значения из структуры, используйте контравариантность, когда вы только намереваетесь поместить универсальные значения в структуру, и используйте точный универсальный тип, когда вы собираетесь делать оба.
Лучший пример, который у меня есть, - это следующее, которое копирует любые номера из одного списка в другой. Он получает только предметы из источника и ставит предметы только в цель.
public static void copy(List<? extends Number> source, List<? super Number> target) {
for(Number number : source) {
target(number);
}
}
Благодаря полномочиям ковариации и контравариантности это работает для случая, подобного этому:
List<Integer> myInts = asList(1,2,3,4);
List<Double> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();
copy(myInts, myObjs);
copy(myDoubles, myObjs);