JavaRush /Java блог /Random UA /Використання varargs при роботі з дженериками

Використання varargs при роботі з дженериками

Стаття з групи Random UA
Вітання! На сьогоднішньому занятті ми продовжимо вивчати дженерики. Так вже вийшло, що це велика тема, але подітися нікуди - це вкрай важлива частина мови :) Коли вивчатимеш документацію Oracle по дженериків або читати гайди в інтернеті, тобі зустрінуться терміни Non-Reifiable Types і Reifiable Types . Що це за слово таке - "Reifiable"? Навіть якщо з англійською все непогано, його навряд чи ти зустрічав. Спробуємо перекласти! Використання varargs при роботі з дженериками - 2
*Дякую, Гугл, ти дуже допоміг -_-*
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). Використання varargs при роботі з дженериками - 4Забруднення може виникнути ось у такій ситуації:
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 " сама себе не прочитає! :) До зустрічі!
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ