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, 21:02
Lika Nauan
Уровень 36
4 января, 15:20
потому что это яндекс
ElenaN
Уровень 37
8 января, 15:47
Мария Ким
Уровень 36
9 января, 07:41
reliable -в гугле написали, reifiable - в яндексе...
ElenaN
Уровень 37
9 января, 22:21
Хаха точно! Теперь исправила))
Anonymous #2502407
Уровень 2
16 августа 2023, 09:45
А если прям в параметрах вместо стринг добавлять, например, интеджер, разве он на попадёт в результате в переданный лист. Ведь лист теряет типизацию внутри метода?
Oraz Janov Backend Developer
9 апреля 2023, 20:24
Скажите пожалуйста, вот это вот мне в жизни понадобиться?
ivan дворник
9 июля 2023, 17:22
да. могу скинуть более понятное объяснение
Ulukay
Уровень 19
25 июля 2023, 11:17
хотелось бы глянуть
ivan дворник
26 июля 2023, 19:17
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));
    }
}
поиграйся с этим кодом. измени аргумент метода на
public static int sum(int numbers) (убрали многоточие)
и посмотри что будет
Денис Стёпшин
Уровень 25
11 августа 2023, 04:13
где вы здесь видите конструктор ?
ivan дворник
13 августа 2023, 15:31
ой. не конструктор а аргумент метода
ivan дворник
13 августа 2023, 15:31
просто конструктор с varargs тоже может быть. я не о том подумал
Fura_IZI
Уровень 48
19 июля 2022, 17:59
Ничего не понял
Serhii Diakonov
Уровень 43
Expert
26 апреля 2023, 10:11
в начале вроде бы еще ничего, а потом какое-то месиво
Джама
Уровень 108
Expert
26 мая 2022, 15:42
Помогите, плжалуйста разобраться:
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) и никак не описывается ситуация где этих списков несколько. Интересует связь между обоими списками и массивом.
Anonymous #3068853
Уровень 3
28 мая 2022, 00:40
stringsLists это не список, а массив списков. Её тип List[]. В момент вызова имеет место: stringsLists[0] == cars1 и stringsLists[1] == cars2.
Джама
Уровень 108
Expert
30 мая 2022, 11:42
Спасибо большое, затупила немного)
Jet
Уровень 24
24 мая 2022, 23:44
24.05.2022 Google: Reifiable - Повторяемый
Anonymous #885613
Уровень 40
21 апреля 2023, 14:22
Мирослав
Уровень 29
Expert
12 мая 2022, 08:26
Конечно не всё так плачебно но все равно как-то так!
Anonymous #3068853
Уровень 3
28 мая 2022, 00:41
Видео начало проигрываться у меня в голове раньше, чем я перешёл по ссылке)))
hidden #2595317
Уровень 45
17 марта 2022, 17:32
Алексей Макаенко PL/SQL Developer в ООО ИТМ
7 ноября 2021, 08:59
В этой статье: "Reifiable-type — это тип, информация о котором полностью доступна во время выполнения. В языке Java к ним относятся примитивы, RAW-TYPES, а также типы, не являющиеся дженериками." В следующей статье "Стирание типов": "Raw Type — это класс-дженерик, из которого удалили его тип." Я один здесь вижу противеречие? Может быть Raw Type относится все-таки к Non-Reifiable Types?
Ян
Уровень 41
16 ноября 2021, 21:26
в последнем примере, наверное, имелось ввиду, что возникнет ClassCastException, при попытке прочитать строку из массива array, а не из stringLists, ведь с массивом stringLists(который является списком), мы ничего не делаем
static void makeHeapPollution(List<String>... stringsLists) {
       Object[] array = stringsLists;
       List<Integer> numbersList = Arrays.asList(66,22,44,12);

       array[0] = numbersList;
       String str = array[0].get(0);
   }
или я что-то не так понял?
Roma
Уровень 39
10 января 2022, 12:14
Дело в том, что при присваивании в array объекта stringLists, оба массива ссылаются на один объект. И если мы изменим массив array, то изменится и массив stringLists который ссылается на него. Тогда в stringLists окажется список numbersList.
Ян
Уровень 41
11 января 2022, 19:43
Спасибо, очень долго с этим разбирался, пытался подогнать под механизм работы, но в конченом счёте так и недоразобрался. После Вашего комментария всё стало понятно(:
Anonymous #3068853
Уровень 3
28 мая 2022, 00:45
"Raw Type — это класс-дженерик, из которого удалили его тип." Это надо понимать неформально. Так же как и "круг это правильный многоугольник с бесконечным числом сторон". На самом деле никакой круг не является правильным многоугольником. И никакой сырой тип не является дженериком.
Edil Kalmamatov
Уровень 35
28 сентября 2021, 08:04
Update от Google 28.09.2021: Reifiable - Реифицируемый
11 марта 2022, 21:03
12.03.2022: Reifiable - Повторяемый :)