Ответ 1
Причины этого основаны на том, как Java реализует дженерики.
Пример массивов
С массивами вы можете это сделать (массивы ковариантны, как объяснили другие)
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Но что произойдет, если вы попытаетесь это сделать?
Number[0] = 3.14; //attempt of heap pollution
Эта последняя строка будет компилироваться просто отлично, но если вы запустите этот код, вы можете получить ArrayStoreException
. Потому что вы пытаетесь поместить двойной массив в целочисленный массив (независимо от того, чтобы получить доступ через ссылку на число).
Это означает, что вы можете обмануть компилятор, но вы не можете обмануть систему типа времени выполнения. И это потому, что массивы - это то, что мы называем повторно идентифицируемыми типами. Это означает, что во время выполнения Java знает, что этот массив фактически был создан как массив целых чисел, к которому просто обращаются через ссылку типа Number[]
.
Итак, как вы можете видеть, одна вещь является фактическим типом объекта, другая вещь - тип ссылки, которую вы используете для доступа к ней, правильно?
Проблема с Java Generics
Теперь проблема с типичными типами Java заключается в том, что информация о типе отбрасывается компилятором, и она недоступна во время выполнения. Этот процесс называется type erasure. Есть хорошая причина для реализации таких обобщений в Java, но это долгая история, и она связана с бинарной совместимостью с уже существующим кодом.
Но важный момент здесь заключается в том, что, поскольку во время выполнения нет информации о типе, нет способа гарантировать, что мы не будем загрязнять кучу.
Например,
List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap polution
Если компилятор Java не останавливает вас от этого, система типов времени выполнения также не сможет вас остановить, потому что во время выполнения нет способа определить, что этот список должен быть только списком целых чисел. Java runtime позволит вам помещать все, что вы хотите в этот список, когда он должен содержать только целые числа, потому что когда он был создан, он был объявлен как список целых чисел.
Таким образом, разработчики 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);
Поскольку вы можете быть уверены, что независимо от того, что содержит фактический список, он может быть увеличен до числа (ведь все, что расширяет Number, является числом, верно?)
Однако вам не разрешено вставлять что-либо в ковариантную структуру.
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);
В этом случае фактическая природа объекта - это список объектов, а через контравариантность вы можете поместить Numbers в него, в основном потому, что все числа имеют Object как их общий предок. Таким образом, все числа являются объектами, и поэтому это действительно.
Однако вы не можете спокойно читать что-либо из этой контравариантной структуры, предполагая, что вы получите номер.
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<Double> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();
copy(myInts, myObjs);
copy(myDoubles, myObjs);