JavaRush /Java блог /Java Developer /Использование varargs при работе с дженериками
Автор
John Selawsky
Senior Java-разработчик и преподаватель в LearningTree

Использование varargs при работе с дженериками

Статья из группы Java Developer
Привет! На сегодняшнем занятии мы продолжим изучать дженерики. Так уж вышло, что это большая тема, но деваться некуда — это крайне важная часть языка :) Когда будешь изучать документацию 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” сама себя не прочитает! :) До встречи!
Комментарии (45)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
ElenaN Уровень 37
26 декабря 2023
Anonymous #2502407 Уровень 2
16 августа 2023
А если прям в параметрах вместо стринг добавлять, например, интеджер, разве он на попадёт в результате в переданный лист. Ведь лист теряет типизацию внутри метода?
Oraz Janov Уровень 34
9 апреля 2023
Скажите пожалуйста, вот это вот мне в жизни понадобиться?
Fura_IZI Уровень 48
19 июля 2022
Ничего не понял
Джама Уровень 108 Expert
26 мая 2022
Помогите, плжалуйста разобраться:

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);
   }
}
У нас за счет vararg появляется возможность передать в метод сразу два списка, в коде при этом я не могу понять как это работает ибо массиву присваивается список (Object[] array = stringsLists) и никак не описывается ситуация где этих списков несколько. Интересует связь между обоими списками и массивом.
Jet Уровень 24
24 мая 2022
24.05.2022 Google: Reifiable - Повторяемый
Мирослав Уровень 29 Expert
12 мая 2022
Конечно не всё так плачебно но все равно как-то так!
hidden #2595317 Уровень 45
17 марта 2022
7 ноября 2021
В этой статье: "Reifiable-type — это тип, информация о котором полностью доступна во время выполнения. В языке Java к ним относятся примитивы, RAW-TYPES, а также типы, не являющиеся дженериками." В следующей статье "Стирание типов": "Raw Type — это класс-дженерик, из которого удалили его тип." Я один здесь вижу противеречие? Может быть Raw Type относится все-таки к Non-Reifiable Types?
Edil Kalmamatov Уровень 35
28 сентября 2021
Update от Google 28.09.2021: Reifiable - Реифицируемый