Причини цього базуються на тому, як Java реалізує дженерики.
Приклад масивів
З масивами ви можете це зробити (масиви є коваріантними)
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Але що буде, якщо ви спробуєте це зробити?
myNumber[0] = 3.14; //attempt of heap pollution
Цей останній рядок складеться чудово, але якщо запустити цей код, ви можете отримати ArrayStoreException
. Тому що ви намагаєтеся поставити двійник у цілий масив (незалежно від того, щоб отримати доступ через посилання числа).
Це означає, що ви можете обдурити компілятор, але не можете обдурити систему типу виконання. І це так, тому що масиви - це те, що ми називаємо повторюваними типами . Це означає, що під час виконання 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 не може гарантувати, що є фактичним типом об'єкта в загальній структурі. Це може бути все, що розширює номер, але компілятор не може бути впевнений. Так ви можете читати, але не писати.
Суперечність
При противаріантності ви можете зробити навпаки. Ви можете помістити речі в загальну структуру, але ви не можете її прочитати.
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);