Hello! Мы продолжаем серию лекций о дженериках. Ранее мы разобрались в общих чертах, что это такое, и зачем нужно. Сегодня поговорим о некоторых особенностях дженериков и рассмотрим некоторые подводные камни при работе с ними. Поехали! В прошлой лекции мы говорor о разнице между Generic Types и Raw Types. Если ты забыл, Raw Type — это класс-дженерик, из которого удалor его тип.
List list = new ArrayList();
Вот пример. Здесь мы не указываем, Howого именно типа an objectы будут помещаться в наш List
. Если попытаться создать такой List
и добавить в него Howие-то an objectы мы увидим в IDEa предупреждение:
“Unchecked call to add(E) as a member of raw type of java.util.List”.
Но также мы говорor о том, что дженерики появorсь только в версии языка Java 5. К моменту ее выхода программисты успели написать кучу codeа с использованием Raw Types, и чтобы он не перестал работать, возможность создания и работы Raw Types в Java сохранилась. Однако эта проблема оказалась гораздо обширнее. Java-code, How ты знаешь, преобразуется в специальный byte-code, который потом выполняется виртуальной machine Java. И если бы в процессе перевода мы помещали в byte-code информацию о типах-параметрах, это сломало бы весь ранее написанный code, ведь до Java 5 ниHowих типов-параметров не существовало! При работе с дженериками есть одна очень важная особенность, о которой необходимо помнить. Она называется “стирание типов” (type erasure). Ее суть заключается в том, что внутри класса не хранится ниHowой информации о его типе-параметре. Эта информация доступна только на этапе компиляции и стирается (становится недоступной) в runtime. Если ты попытаешься положить an object не того типа в свой List<String>
, компилятор выдаст ошибку. Этого How раз и добивались создатели языка, создавая дженерики — проверки на этапе компиляции. Но когда весь написанный тобой Java-code превратится в byte-code, в нем не будет информации о типах-параметрах. Внутри byte-codeа твой список List<Cat>
cats не будет отличаться от List<String>
strings. В byte-codeе ничто не будет говорить о том, что cats
— это список an objectов Cat
. Информация об этом сотрется во время компиляции, и в byte code попадет только информация о том, что у тебя в программе есть некий список List<Object> cats
. Давай посмотрим How это работает:
public class TestClass<T> {
private T value1;
private T value2;
public void printValues() {
System.out.println(value1);
System.out.println(value2);
}
public static <T> TestClass<T> createAndAdd2Values(Object o1, Object o2) {
TestClass<T> result = new TestClass<>();
result.value1 = (T) o1;
result.value2 = (T) o2;
return result;
}
public static void main(String[] args) {
Double d = 22.111;
String s = "Test String";
TestClass<Integer> test = createAndAdd2Values(d, s);
test.printValues();
}
}
Мы создали собственный дженерик-класс TestClass
. Он довольно прост: по сути это небольшая “коллекция” на 2 an object, которые помещаются туда сразу же при создании an object. В качестве полей у него 2 an object T
. При выполнении метода createAndAdd2Values()
должно произойти приведение двух переданных an objectов Object a
и Object b
к нашему типу T
, после чего они будут добавлены в an object TestClass
. В методе main()
мы создаем TestClass<Integer>
, то есть в качестве T
у нас будет Integer
. Но при этом в метод createAndAdd2Values()
мы передаем число Double
и an object String
. Как ты думаешь, сработает ли наша программа? Ведь в качестве типа-параметра мы указали Integer
, а String
точно нельзя привести к Integer
! Давай запустим метод main()
и проверим. Вывод в консоль: 22.111 Test String Неожиданный результат! Почему такое произошло? Именно из-за стирания типов. Во время компиляции codeа информация о типе-параметре Integer
нашего an object TestClass<Integer> test
стерлась. Он превратился в TestClass<Object> test
. Наши параметры Double
и String
без проблем преобразовались в Object
(а не в Integer
, How мы того ожидали!) и спокойно добавorсь в TestClass
. Вот еще один простой, но очень показательный пример стирания типов:
import java.util.ArrayList;
import java.util.List;
public class Main {
private class Cat {
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
List<Cat> cats = new ArrayList<>();
System.out.println(strings.getClass() == numbers.getClass());
System.out.println(numbers.getClass() == cats.getClass());
}
}
Вывод в консоль: true true Казалось бы, мы создали коллекции с тремя разными типами-параметрами — String
, Integer
, и созданный нами класс Cat
. Но во время преобразования в byte-code все три списка превратorсь в List<Object>
, поэтому при выполнении программа говорит нам, что во всех трех случаях у нас используется один и тот же класс.
Стирание типов при работе с массивами и дженериками
Есть один очень важный момент, который необходимо четко понимать при работе с массивами и дженериками (например,List
). Также его стоит учитывать при выборе структуры данных для твоей программы. Дженерики подвержены стиранию типов. Информация о типе-параметре недоступна во время выполнения программы. В отличие от них, массивы знают и могут использовать информацию о своем типе данных во время выполнения программы. Попытка поместить в массив meaning неверного типа приведет к исключению:
public class Main2 {
public static void main(String[] args) {
Object x[] = new String[3];
x[0] = new Integer(222);
}
}
Вывод в консоль:
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
Из-за того, что между массивами и дженериками есть такая большая разница, у них могут возникнуть проблемы с совместимостью. Прежде всего, ты не можешь создать массив an objectов-дженериков or даже просто типизированный массив. Звучит немного непонятно? Давай рассмотрим наглядно. К примеру, ты не сможешь сделать в Java ничего из этого:
new List<T>[]
new List<String>[]
new T[]
Если мы попытаемся создать массив списков List<String>
, получим ошибку компиляции generic array creation:
import java.util.List;
public class Main2 {
public static void main(String[] args) {
//ошибка компиляции! Generic array creation
List<String>[] stringLists = new List<String>[1];
}
}
Но для чего это сделано? Почему создание таких массивов запрещено? Это все — для обеспечения типобезопасности. Если бы компилятор позволял нам создавать такие массивы из an objectов-дженериков, мы могли бы заработать кучу проблем. Вот простой пример из книги Джошуа Блоха “Effective Java”:
public static void main(String[] args) {
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = Arrays.asList(42, 65, 44); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)
}
Давай представим, что создание массива List<String>[] stringLists
было бы разрешено, и компилятор бы не ругался. Вот Howих дел мы могли бы наворотить в этом случае: В строке 1 мы создаем массив листов List<String>[] stringLists
. Наш массив вмещает в себя один List<String>
. В строке 2 мы создаем список чисел List<Integer>
. В строке 3 мы присваиваем наш массив List<String>[]
в переменную Object[] objects
. Язык Java позволяет это делать: в массив an objectов X
можно помещать и an objectы X
, и an objectы всех дочерних классов Х
. Соответственно, в массив Objects
можно поместить вообще все что угодно. В строке 4 мы подменяем единственный элемент массива objects (List<String>)
на список List<Integer>
. В результате мы поместor List<Integer>
в наш массив, который предназначался только для хранения List<String>
! С ошибкой же мы столкнемся только когда code дойдет до строки 5. Во время выполнения программы будет выброшено исключение ClassCastException
. Поэтому запрет на создание таких массивов и был введен в язык Java — это позволяет нам избегать подобных ситуаций.
Как можно обойти стирание типов?
What ж, стирание типов мы изучor. Давай попробуем обмануть систему! :) Задача: У нас есть класс-дженерикTestClass<T>
. Нам нужно создать в нем метод createNewT()
, который будет создавать и возвращать новый an object типа Т
. Но ведь это невозможно сделать, так? Вся информация о типе Т
будет стерта во время компиляции, и в процессе работы программы мы не сможем узнать, an object Howого именно типа нам нужно создать. На самом деле, есть один хитрый способ. Ты наверняка помнишь, что в Java есть класс Class
. Используя его, мы можем получить класс любого нашего an object:
public class Main2 {
public static void main(String[] args) {
Class classInt = Integer.class;
Class classString = String.class;
System.out.println(classInt);
System.out.println(classString);
}
}
Вывод в консоль:
class java.lang.Integer
class java.lang.String
Но вот одна особенность, о которой мы не говорor. В documentации Oracle ты увидишь, что класс Class — это дженерик! В documentации написано: “Т — это тип класса, моделируемого этим an objectом Class”. Если перевести это с языка documentации на человеческий, это означает, что классом для an object Integer.class
является не просто Class
, а Class<Integer>
. Типом an object string.class
является не просто Class
, Class<String>
, и т.д. Если все еще непонятно, попробуй добавить тип-параметр к предыдущему примеру:
public class Main2 {
public static void main(String[] args) {
Class<Integer> classInt = Integer.class;
//ошибка компиляции!
Class<String> classInt2 = Integer.class;
Class<String> classString = String.class;
//ошибка компиляции!
Class<Double> classString2 = String.class;
}
}
И вот теперь, используя это знание, мы можем обойти стирание типов и решить нашу задачу! Попробуем получить информацию о типе-параметре. Его роль будет играть класс MySecretClass
:
public class MySecretClass {
public MySecretClass() {
System.out.println("Объект секретного класса успешно создан!");
}
}
А вот How мы используем на практике наше решение:
public class TestClass<T> {
Class<T> typeParameterClass;
public TestClass(Class<T> typeParameterClass) {
this.typeParameterClass = typeParameterClass;
}
public T createNewT() throws IllegalAccessException, InstantiationException {
T t = typeParameterClass.newInstance();
return t;
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
MySecretClass secret = testString.createNewT();
}
}
Вывод в консоль:
Объект секретного класса успешно создан!
Мы просто передали нужный класс-параметр в конструктор нашего класса-дженерика:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
Благодаря этому мы сохранor информацию о типе-параметре и уберегли ее от стирания. В итоге мы смогли создать an object T
! :) На этом сегодняшняя лекция подходит к концу. О стирании типов всегда необходимо помнить при работе с дженериками. Выглядит это дело не очень удобно, но нужно понимать — дженерики не были частью языка Java при его создании. Это позже прикрученная возможность, которая помогает нам создавать типизированные коллекции и отлавливать ошибки на этапе компиляции. В некоторых других языках, где дженерики появлялись с первой версии, стирание типов отсутствует (например, в C#). Впрочем, мы не закончor изучение дженериков! На следующей лекции ты познакомишься с еще несколькими особенностями работы с ними. А пока было бы неплохо решить пару задач! :)
GO TO FULL VERSION