Привет! На сегодняшнем занятии мы продолжим изучать дженерики. Так уж вышло, что это большая тема, но деваться некуда — это крайне важная часть языка :)
Когда будешь изучать документацию Oracle по дженерикам или читать гайды в интернете, тебе встретятся термины Non-Reifiable Types и Reifiable Types.
Что это за слово такое — “Reifiable”? Даже если с английским все неплохо, его ты вряд ли встречал. Попробуем перевести!
*спасибо, Гугл, ты очень помог -_-*
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).
Загрязнение может возникнуть вот в такой ситуации:
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” сама себя не прочитает! :)
До встречи!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ