Масиви коваріантні
Кажуть, що масиви є коваріантними, що в основному означає, що, зважаючи на правила підтипу Java, масив типів T[]
може містити елементи типу T
або будь-який підтип T
. Наприклад
Number[] numbers = new Number[3];
numbers[0] = newInteger(10);
numbers[1] = newDouble(3.14);
numbers[2] = newByte(0);
Але не тільки це, правила S[]
підтипу Java також стверджують, що масив є підтипом масиву, T[]
якщо S
є підтипом T
, отже, щось подібне також є дійсним:
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Тому що згідно з правилами Integer[]
підтипу на Java, масив є підтипом масиву, Number[]
оскільки Integer є підтипом Number.
Але це правило підтипу може призвести до цікавого питання: що буде, якщо ми спробуємо це зробити?
myNumber[0] = 3.14; //attempt of heap pollution
Цей останній рядок складеться просто чудово, але якщо ми запустимо цей код, ми отримаємо, ArrayStoreException
тому що ми намагаємось поставити двійник у цілий масив. Те, що ми отримуємо доступ до масиву через посилання Number, тут не має значення, що важливо, це те, що масив є масивом цілих чисел.
Це означає, що ми можемо обдурити компілятор, але не можемо обдурити систему типу виконання. І це так, тому що масиви - це те, що ми називаємо повторюваним типом. Це означає, що під час виконання Java знає, що цей масив був фактично створений як масив цілих чисел, до якого просто трапляється доступ через посилання типу Number[]
.
Отже, як ми бачимо, одна річ - це фактичний тип об’єкта, інша річ - тип посилання, який ми використовуємо для доступу до нього, правда?
Проблема з Java Generics
Тепер проблема з загальними типами на Java полягає в тому, що компілятор відкидає інформацію про тип для параметрів типу після завершення компіляції коду; тому інформація про цей тип недоступна під час виконання. Цей процес називається стиранням типу . Існують вагомі причини для використання таких дженериків на Java, але це довга історія, і це стосується бінарної сумісності з раніше існуючим кодом.
Важливим моментом тут є те, що оскільки під час роботи немає інформації про тип, немає ніякого способу гарантувати, що ми не здійснюємо забруднення від купи.
Розглянемо наступний небезпечний код:
List<Integer> myInts = newArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap polution
Якщо компілятор Java не заважає нам це робити, система типу запуску не може нас також зупинити, оскільки немає можливості визначити, що цей список повинен бути лише списком цілих чисел. Час виконання Java дозволить нам включити все, що ми хочемо, до цього списку, коли він повинен містити лише цілі числа, тому що, коли він був створений, він був оголошений як список цілих чисел. Ось чому компілятор відхиляє рядок №4, оскільки це небезпечно і, якщо це дозволено, може порушити припущення системи типу.
Таким чином, дизайнери Java переконалися, що ми не можемо обдурити компілятор. Якщо ми не можемо обдурити компілятор (як це можна зробити з масивами), ми також не можемо обдурити систему типу запуску.
Як такий, ми кажемо, що родові типи не підлягають повторенню, оскільки під час виконання ми не можемо визначити справжню природу родового типу.
Я пропустив деякі частини цих відповідей, ви можете прочитати повну статтю тут:
https://dzone.com/articles/covariance-and-contravariance