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