Вітання! На сьогоднішньому занятті ми продовжимо вивчати дженерики. Так вже вийшло, що це велика тема, але подітися нікуди - це вкрай важлива частина мови :) Коли вивчатимеш документацію Oracle по дженериків або читати гайди в інтернеті, тобі зустрінуться терміни Non-Reifiable Types і Reifiable Types . Що це за слово таке - "Reifiable"? Навіть якщо з англійською все непогано, його навряд чи ти зустрічав. Спробуємо перекласти!
*Дякую, Гугл, ти дуже допоміг -_-*
Reifiable-type – це тип, інформація про який повністю доступна під час виконання. У мові Java до них відносяться примітиви, raw-types, а також типи, що не є дженеріками. Навпаки, Non-Reifiable Types – це типи, інформація про які стирається та стає недоступною під час виконання. Це саме дженерики - List<String> , List<Integer> і т.д.
До речі, ти пам'ятаєш, що таке varargs ?
Якщо раптом ти забув, то це аргументи змінної довжини. Вони стануть у нагоді в ситуаціях, коли ми не знаємо, скільки точно аргументів може бути передано в наш метод. Наприклад, якщо ми маємо клас-калькулятор і в ньому є методsum
. У метод sum()
можна передати 2 числа, 3, 5 або взагалі скільки завгодно. Було б дуже дивно щоразу перевантажувати метод sum()
, щоб врахувати усі можливі варіанти. Натомість ми можемо зробити так:
public class SimpleCalculator {
public static int sum(int...numbers) {
int result = 0;
for(int i : numbers) {
result += i;
}
return result;
}
public static void main(String[] args) {
System.out.println(sum(1,2,3,4,5));
System.out.println(sum(2,9));
}
}
Виведення в консоль:
15
11
Так ось, використання varargs
в поєднанні з дженериками є деякі важливі особливості. Давай розглянемо цей код:
import javafx.util.Pair;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static <E> void addAll(List<E> list, E... array) {
for (E element : array) {
list.add(element);
}
}
public static void main(String[] args) {
addAll(new ArrayList<String>(), // здесь все нормально
"Leonardo da Vinci",
"Vasco de Gama"
);
// а здесь мы получаем предупреждение
addAll(new ArrayList<Pair<String, String>>(),
new Pair<String, String>("Leonardo", "da Vinci"),
new Pair<String, String>("Vasco", "de Gama")
);
}
}
Метод addAll()
приймає на вхід список List<E>
та будь-яку кількість об'єктів E
, після чого додає всі ці об'єкти до списку. У методі main()
ми двічі викликаємо наш метод addAll()
. Вперше ми додаємо у List
два звичайні рядки. Тут усе гаразд. Вдруге ми додаємо у List
два об'єкти Pair<String, String>
. І ось тут ми зненацька отримуємо попередження:
Unchecked generics array creation for varargs parameter
Що це означає? Чому ми отримуємо попередження і причому тут взагалі array
? Array
- Це масив, а в нашому коді немає ніяких масивів! Почнемо з другого. У попередженні згадується масив, тому що компілятор перетворює аргументи змінної довжини (varargs) масив. Іншими словами, сигнатура нашого методу addAll()
:
public static <E> void addAll(List<E> list, E... array)
Насправді виглядає так:
public static <E> void addAll(List<E> list, E[] array)
Тобто в методі main()
компілятор перетворює наш код на це:
public static void main(String[] args) {
addAll(new ArrayList<String>(),
new String[] {
"Leonardo da Vinci",
"Vasco de Gama"
}
);
addAll(new ArrayList<Pair<String,String>>(),
new Pair<String,String>[] {
new Pair<String,String>("Leonardo","da Vinci"),
new Pair<String,String>("Vasco","de Gama")
}
);
}
З масивом String
все гаразд. А ось із масивом Pair<String, String>
— ні. Справа в тому, що Pair<String, String>
це Non-Reifiable Type. При компіляції вся інформація про типи-параметри (<String, String>) буде стерта. Створення масивів з Non-Reifiable Type у Java заборонено . Ти можеш у цьому переконатися, якщо спробуєш створити вручну масив Pair<String, String>
public static void main(String[] args) {
// ошибка компиляции! Generic array creation
Pair<String, String>[] array = new Pair<String, String>[10];
}
Причина очевидна – типобезпека. Як ти пам'ятаєш, при створенні масиву тобі обов'язково потрібно вказати, які об'єкти (або примітиви) зберігатиме цей масив.
int array[] = new int[10];
На одному з минулих занять ми докладно розібрали механізм стирання типів. Так ось, в даному випадку ми в результаті стирання типів втратабо інформацію про те, що в наших об'єктах Pair
зберігалися пари <String, String>
. Створення масиву буде небезпечним. При використанні методів з varargs
дженериками обов'язково пам'ятай про стирання типів і про те, як саме воно працює. Якщо ти абсолютно точно впевнений у написаному коді, і знаєш, що він не викличе жодних проблем, ти можеш відключити пов'язані з varargs
попередження за допомогою анотації.@SafeVarargs
@SafeVarargs
public static <E> void addAll(List<E> list, E... array) {
for (E element : array) {
list.add(element);
}
}
Якщо ти додаси до свого методу цю анотацію, попередження, з яким ми зіткнулися раніше, не з'являтиметься. Ще одна можлива проблема при спільному використанні varargs
та дженериків, - забруднення купи (heap pollution). Забруднення може виникнути ось у такій ситуації:
import java.util.ArrayList;
import java.util.List;
public class Main {
static List<String> makeHeapPollution() {
List numbers = new ArrayList<Number>();
numbers.add(1);
List<String> strings = numbers;
strings.add("");
return strings;
}
public static void main(String[] args) {
List<String> stringsWithHeapPollution = makeHeapPollution();
System.out.println(stringsWithHeapPollution.get(0));
}
}
Виведення в консоль:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Говорячи простою мовою, забруднення купи - це ситуація, при якій у купі повинні знаходитися об'єкти типу А
, але в результаті там виявляються об'єкти типу B
через помилки, пов'язані з типобезпекою. У нашому прикладі це відбувається. Спочатку ми створабо Raw-змінну numbers
і привласнабо їй дженерик-колекцію ArrayList<Number>
. Після цього ми додали туди число 1
.
List<String> strings = numbers;
У цьому рядку компілятор намагався попередити нас про ймовірні помилки, видавши попередження “ Unchecked assignment... ”, але ми проігнорували його. В результаті у нас є дженерик-змінна типу List<String>
, яка вказує на дженерик-колекцію типу ArrayList<Number>
. Ця ситуація може призвести до неприємностей! Так відбувається. Використовуючи нашу нову змінну, ми додаємо до колекції рядок. Відбулося забруднення купи - ми додали в типізовану колекцію спочатку число, а потім рядок. Компілятор нас попереджав, але ми його попередження проігнорували, отримавши результат ClassCastException
лише під час роботи програми. До чого ж тут varargs
? Використання varargs
з дженериками може призвести до забруднення купи. Ось простий приклад:
import java.util.Arrays;
import java.util.List;
public class Main {
static void makeHeapPollution(List<String>... stringsLists) {
Object[] array = stringsLists;
List<Integer> numbersList = Arrays.asList(66,22,44,12);
array[0] = numbersList;
String str = stringsLists[0].get(0);
}
public static void main(String[] args) {
List<String> cars1 = Arrays.asList("Ford", "Fiat", "Kia");
List<String> cars2 = Arrays.asList("Ferrari", "Bugatti", "Zaporozhets");
makeHeapPollution(cars1, cars2);
}
}
Що тут відбувається? Через стирання типів наші аркуші-параметри (назватимемо їх “листами” замість “списків” для зручності) —
List<String>...stringsLists
— перетворяться на масив аркушів — List[]
з невідомим типом (не забувай, що varargs внаслідок компіляції перетворюється на звичайний масив). Через це ми легко можемо зробити присвоєння в змінну Object[] array
в першому рядку методу - типи з наших листів стерлися! І тепер у нас є змінна типу Object[]
, куди можна додавати взагалі будь-що - всі об'єкти в Java успадковуються від Object
! Зараз у нас лише масив рядкових аркушів. Але завдяки використанню varargs
та стирання типів ми легко можемо додати до них лист, що складається з чисел, що ми робимо. В результаті ми забруднюємо купу через змішування різних типів об'єктів. Результатом буде той самий виняток ClassCastException
при спробі прочитати рядок з масиву. Виведення в консоль:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Ось до таких несподіваних наслідків може призвести використання простого, начебто, механізму varargs
:) А наша сьогоднішня лекція на цьому добігає кінця. Не забудь вирішити кілька завдань, а якщо залишаться час і сабо — вивчити додаткову літературу. " Effective Java " сама себе не прочитає! :) До зустрічі!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ