Varargs и дженерики уживаются друг с другом плохо, и причина в стирании типов. Параметр
E... array компилятор превращает в обычный массив
E[], а массив из дженерик-типа в Java создать нельзя: к моменту выполнения информация о типе-параметре уже стерта. Отсюда предупреждение
Unchecked generics array creation for varargs parameter и вполне реальный риск загрязнения кучи, когда в типизированную коллекцию попадают чужие объекты и программа падает с
ClassCastException. Заглушить предупреждение можно аннотацией
@SafeVarargs, но только если метод действительно ничего не пишет в этот массив.
Кратко
- Reifiable-типы это те, о которых информация о типе доступна во время выполнения: примитивы, обычные классы, raw-типы. Non-Reifiable это дженерики, их параметры стираются при компиляции.
- Параметр переменной длины
E... array на самом деле обычный массив E[], компилятор превращает одно в другое сам.
- Массив из Non-Reifiable типа создать нельзя:
new Map.Entry<String, String>[10] это ошибка компиляции Generic array creation.
- Когда в varargs попадает дженерик, компилятор выдает предупреждение
Unchecked generics array creation for varargs parameter.
- Заглушить его умеет
@SafeVarargs, но вешать эту аннотацию можно только на static-, final- и private-методы и на конструкторы, и только если метод и правда безопасен.
- Загрязнение кучи это когда переменная дженерик-типа указывает на объект другого типа. Расплата приходит позже и в неожиданном месте, в виде
ClassCastException.
Привет! На сегодняшнем занятии мы продолжим изучать
дженерики. Так уж вышло, что это большая тема, но деваться некуда, это крайне важная часть языка :)
Reifiable и Non-Reifiable типы
Когда будешь изучать документацию Oracle по дженерикам или читать гайды в интернете, тебе встретятся термины
Non-Reifiable Types и
Reifiable Types.
Что это за слово такое, “Reifiable”? Даже если с английским все неплохо, его ты вряд ли встречал. Попробуем перевести!
*спасибо, Гугл, ты очень помог -_-*
Reifiable-type это тип, информация о котором полностью доступна во время выполнения.
В языке Java к ним относятся примитивы, raw-types, а также типы, не являющиеся дженериками.
Спецификация языка добавляет к этому списку еще два случая: параметризованный тип, у которого все параметры это неограниченные wildcards (то есть
List<?>), и массив, у которого reifiable сам тип элемента.
Напротив,
Non-Reifiable Types это типы, информация о которых стирается и становится недоступной во время выполнения. Это как раз дженерики:
List<String>,
List<Integer> и т.д. Разницу удобно держать рядом:
|
Reifiable |
Non-Reifiable |
| Информация о типе во время выполнения |
Доступна полностью |
Стирается при компиляции |
| Примеры |
int, String, List (raw), List<?> |
List<String>, Map.Entry<String, String>, параметр E |
| Можно ли создать массив |
Да |
Нет, ошибка компиляции Generic array creation |
| Как ведет себя в varargs |
Молча, без предупреждений |
Компилятор предупреждает про unchecked generics array creation |
Кстати, ты помнишь, что такое 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 встречает дженерик
Так вот, у использования
varargs в сочетании с дженериками есть некоторые важные особенности.
Давай рассмотрим этот код:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
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<Map.Entry<String, String>>(),
Map.entry("Leonardo", "da Vinci"),
Map.entry("Vasco", "de Gama")
);
}
}
Метод
addAll() принимает на вход список
List<E> и любое количество объектов
E, после чего добавляет все эти объекты в список.
В методе
main() мы дважды вызываем наш метод
addAll().
В первый раз мы добавляем в
List две обычные строки. Здесь все в порядке.
Во второй раз мы добавляем в
List два объекта
Map.Entry<String, String>. Фабричный метод
Map.entry() появился в Java 9 и отдает неизменяемую пару ключ-значение, отдельный класс для этого заводить не нужно.
И вот здесь мы неожиданно получаем предупреждение:
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<Map.Entry<String,String>>(),
new Map.Entry<String,String>[] {
Map.entry("Leonardo","da Vinci"),
Map.entry("Vasco","de Gama")
}
);
}
С массивом
String все нормально.
А вот с массивом
Map.Entry<String, String> уже нет.
Почему массив из Non-Reifiable типа создать нельзя
Дело в том, что
Map.Entry<String, String> это Non-Reifiable Type. При компиляции вся информация о типах-параметрах (<String, String>) будет стерта.
Создание массивов из Non-Reifiable Type в Java запрещено.
Ты можешь в этом убедиться, если попробуешь вручную создать массив Map.Entry<String, String>
public static void main(String[] args) {
// ошибка компиляции! Generic array creation
Map.Entry<String, String>[] array = new Map.Entry<String, String>[10];
}
Причина очевидна: типобезопасность. Как ты помнишь, при создании массива тебе обязательно нужно указать, какие объекты (или примитивы) будет хранить этот массив.
int array[] = new int[10];
На одном из прошлых занятий мы подробно разобрали механизм
стирания типов.
Так вот, в данном случае мы в результате стирания типов потеряли информацию о том, что в наших объектах
Map.Entry хранились пары
<String, String>. Создание массива будет небезопасным.
При использовании методов с
varargs и дженериками обязательно помни о стирании типов и о том, как именно оно работает.
Аннотация @SafeVarargs
Если ты совершенно точно уверен в написанном коде и знаешь, что он не вызовет никаких проблем, ты можешь отключить связанные с
varargs предупреждения при помощи аннотации
@SafeVarargs
@SafeVarargs
public static <E> void addAll(List<E> list, E... array) {
for (E element : array) {
list.add(element);
}
}
Если ты добавишь к своему методу эту аннотацию, предупреждение, с которым мы столкнулись ранее, появляться не будет.
Только вешать ее можно не на любой метод.
Документация формулирует это как ошибку компиляции: аннотацию нельзя ставить на метод с фиксированным числом аргументов и на метод переменной длины, который «is neither static nor final nor private». То есть подходят статические методы, финальные методы, конструкторы, а с Java 9 еще и приватные методы. Обычный нестатический метод так пометить не выйдет, и это логично: его могут переопределить в наследнике, а за безопасность чужой реализации автор аннотации отвечать не может.
Загрязнение кучи (heap pollution)
Еще одна возможная проблема при совместном использовании
varargs и дженериков это загрязнение кучи (heap pollution).
![Городские небоскребы в густом смоге, иллюстрация к загрязнению кучи]()
Загрязнение может возникнуть вот в такой ситуации:
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: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
Хвост про модули и загрузчик в сообщении появился в Java 9, до нее строка была короче.
Говоря простым языком, загрязнение кучи это ситуация, при которой в куче должны находиться объекты типа
А, но в результате там оказываются объекты типа
B, и все из-за ошибок, связанных с типобезопасностью.
В нашем примере это и происходит. Сначала мы создали Raw-переменную
numbers, и присвоили ей дженерик-коллекцию
ArrayList<Number>. После этого мы добавили туда число
1.
List<String> strings = numbers;
В этой строке компилятор пытался предупредить нас о вероятных ошибках, выдав предупреждение “
Unchecked assignment...”, но мы проигнорировали его.
В результате у нас есть дженерик-переменная типа
List<String>, которая указывает на дженерик-коллекцию типа
ArrayList<Number>. Эта ситуация явно может привести к неприятностям!
Так и происходит. Используя нашу новую переменную, мы добавляем в коллекцию строку. Произошло загрязнение кучи: мы добавили в типизированную коллекцию сначала число, а потом строку. Компилятор нас предупреждал, но мы его предупреждение проигнорировали, получив в результате
ClassCastException только во время работы программы.
Причем же здесь varargs
Причем же здесь
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: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
Вот к таким неожиданным последствиям может привести использование простого, казалось бы, механизма
varargs :)
Как не наступать на эти грабли
Рецепт короткий, и он ровно из той книги, что советую в самом конце.
Первое и главное: если параметр переменной длины у тебя дженерик, сперва подумай, нужен ли вообще
varargs. Чаще всего его спокойно заменяет обычный
List<E>, и проблема исчезает сама собой. Список это нормальный объект, а не массив, и создавать массив Non-Reifiable типа уже никому не приходится.
// вместо этого
static <E> void addAll(List<E> list, E... array)
// пиши это
static <E> void addAll(List<E> list, List<E> elements)
Вызов станет чуть длиннее (
List.of("a", "b") вместо просто
"a", "b"), зато компилятор замолчит, потому что предупреждать будет не о чем.
Второе: если
varargs все-таки нужен, прогони метод по двум пунктам, прежде чем вешать
@SafeVarargs.
- Метод ничего не записывает в массив-параметр. Именно запись и ломает типобезопасность.
- Метод не отдает этот массив наружу: не возвращает его и не передает туда, где массив могут сохранить. Как только ссылка ушла из метода, отвечать за нее ты уже не можешь.
Оба пункта выполняются, значит метод и правда безопасен и аннотация уместна. Хотя бы один нарушен, значит аннотация просто спрячет предупреждение, а
ClassCastException никуда не денется. В
документации @SafeVarargs на этот счет есть говорящий пример: метод помечен аннотацией, а рядом стоит комментарий «Not actually safe!», и это ровно тот случай, который мы разобрали выше.
А наша сегодняшняя лекция на этом подходит к концу.
Не забудь решить пару задач, а если останутся время и силы, изучить дополнительную литературу.
“
Effective Java” сама себя не прочитает! :)
До встречи!
Определение, кстати, есть и в спецификации языка:
раздел 4.12.2 называет загрязнением кучи ситуацию, когда переменная параметризованного типа ссылается на объект, который этому типу не соответствует.
Вопросы и ответы
Что такое Reifiable и Non-Reifiable типы в Java?
Reifiable это типы, полная информация о которых доступна во время выполнения программы: примитивы, обычные классы и интерфейсы, raw-типы, а также параметризованные типы с одними лишь неограниченными wildcards вроде
List. Non-Reifiable это дженерики: их параметры компилятор стирает, и в рантайме
List и
List выглядят одинаково. Именно из-за этого дженерики нельзя класть в массив.
Почему компилятор ругается на varargs с дженериками?
Потому что параметр переменной длины это на самом деле массив. Запись
E... array компилятор разворачивает в
E[] array, а на месте вызова создает массив из переданных аргументов. Если
E это дженерик-тип, получается массив Non-Reifiable типа, который создавать нельзя. Компилятор все же создает его, но честно предупреждает:
Unchecked generics array creation for varargs parameter.
Почему в Java нельзя создать массив дженерик-типа?
Из-за типобезопасности. Массив всегда знает тип своих элементов и проверяет его при записи. У дженерика тип-параметр к моменту выполнения стерт, проверять массиву нечего, и в него можно было бы положить что угодно. Поэтому строка вида
new Map.Entry[10] с указанными параметрами это ошибка компиляции
Generic array creation.
Когда можно ставить аннотацию SafeVarargs?
На статические методы, на финальные методы, на конструкторы, а начиная с Java 9 еще и на приватные методы. Документация формулирует ограничение как ошибку компиляции: метод переменной длины, который не является ни
static, ни
final, ни
private, аннотировать нельзя. Причина в переопределении: наследник может подменить реализацию, и обещание безопасности перестанет действовать. И главное: аннотация только убирает предупреждение, безопасным код она не делает.
Что такое загрязнение кучи?
Ситуация, когда переменная параметризованного типа ссылается на объект, который на самом деле другого типа. Спецификация языка называет это heap pollution. Возникает оно после операций с raw-типами или после того, как массив с non-reifiable элементами присвоили переменной более общего типа. Компилятор при этом выдает unchecked-предупреждение, но если его проигнорировать, программа соберется и запустится.
Почему ClassCastException возникает не там, где ошибка?
Потому что реальная ошибка происходит при записи, а падает программа при чтении. В момент, когда в список строк кладут число, никакой проверки нет: тип-параметр стерт, для виртуальной машины это просто список объектов. Проверка появляется там, где значение достают и приводят к объявленному типу. Компилятор вставляет туда приведение к
String, оно и падает. Поэтому unchecked-предупреждения лучше не игнорировать: они указывают на настоящее место ошибки.
Читайте также
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ