Привіт! На сьогоднішньому занятті ми продовжимо вивчати дженеріки. Так вже сталося, що це велика тема, але діватися нікуди — це надзвичайно важлива частина мови :)
Коли будеш вивчати документацію 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() компілятор перетворює наш код на це:, яка вказує на дженерік-колекцію типу 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” сама себе не прочитає! :)
До зустрічі!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ