Привет! На сегодняшнем занятии мы продолжим изучать дженерики. Так уж вышло, что это большая тема, но деваться некуда — это крайне важная часть языка :) Когда будешь изучать документацию Oracle по дженерикам или читать гайды в интернете, тебе встретятся термины Non-Reifiable Types и Reifiable Types. Использование varargs при работе с дженериками - 1Что это за слово такое — “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 при работе с дженериками - 3Загрязнение может возникнуть вот в такой ситуации:
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” сама себя не прочитает! :) До встречи!