JavaRush/Java блог/Java Developer/Стирание типов
Автор
Горковенко Андрей
Фронтенд-разработчик в NFON AG

Стирание типов

Статья из группы Java Developer
участников
Привет! Мы продолжаем серию лекций о дженериках. Ранее мы разобрались в общих чертах, что это такое, и зачем нужно. Сегодня поговорим о некоторых особенностях дженериков и рассмотрим некоторые подводные камни при работе с ними. Поехали! Стирание типов - 1В прошлой лекции мы говорили о разнице между Generic Types и Raw Types. Если ты забыл, Raw Type — это класс-дженерик, из которого удалили его тип.
List list = new ArrayList();
Вот пример. Здесь мы не указываем, какого именно типа объекты будут помещаться в наш List. Если попытаться создать такой List и добавить в него какие-то объекты мы увидим в IDEa предупреждение:

“Unchecked call to add(E) as a member of raw type of java.util.List”.
Но также мы говорили о том, что дженерики появились только в версии языка Java 5. К моменту ее выхода программисты успели написать кучу кода с использованием Raw Types, и чтобы он не перестал работать, возможность создания и работы Raw Types в Java сохранилась. Однако эта проблема оказалась гораздо обширнее. Java-код, как ты знаешь, преобразуется в специальный байт-код, который потом выполняется виртуальной машиной Java. И если бы в процессе перевода мы помещали в байт-код информацию о типах-параметрах, это сломало бы весь ранее написанный код, ведь до Java 5 никаких типов-параметров не существовало! При работе с дженериками есть одна очень важная особенность, о которой необходимо помнить. Она называется “стирание типов” (type erasure). Ее суть заключается в том, что внутри класса не хранится никакой информации о его типе-параметре. Эта информация доступна только на этапе компиляции и стирается (становится недоступной) в runtime. Если ты попытаешься положить объект не того типа в свой List<String>, компилятор выдаст ошибку. Этого как раз и добивались создатели языка, создавая дженерики — проверки на этапе компиляции. Но когда весь написанный тобой Java-код превратится в байт-код, в нем не будет информации о типах-параметрах. Внутри байт-кода твой список List<Cat> cats не будет отличаться от List<String> strings. В байт-коде ничто не будет говорить о том, что cats — это список объектов Cat. Информация об этом сотрется во время компиляции, и в байт код попадет только информация о том, что у тебя в программе есть некий список List<Object> cats. Давай посмотрим как это работает:
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 объекта, которые помещаются туда сразу же при создании объекта. В качестве полей у него 2 объекта T. При выполнении метода createAndAdd2Values() должно произойти приведение двух переданных объектов Object a и Object b к нашему типу T, после чего они будут добавлены в объект TestClass. В методе main() мы создаем TestClass<Integer>, то есть в качестве T у нас будет Integer. Но при этом в метод createAndAdd2Values() мы передаем число Double и объект String. Как ты думаешь, сработает ли наша программа? Ведь в качестве типа-параметра мы указали Integer, а String точно нельзя привести к Integer! Давай запустим метод main() и проверим. Вывод в консоль: 22.111 Test String Неожиданный результат! Почему такое произошло? Именно из-за стирания типов. Во время компиляции кода информация о типе-параметре Integer нашего объекта TestClass<Integer> test стерлась. Он превратился в TestClass<Object> test. Наши параметры Double и String без проблем преобразовались в Object (а не в Integer, как мы того ожидали!) и спокойно добавились в 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. Но во время преобразования в байт-код все три списка превратились в List<Object>, поэтому при выполнении программа говорит нам, что во всех трех случаях у нас используется один и тот же класс.

Стирание типов при работе с массивами и дженериками

Есть один очень важный момент, который необходимо четко понимать при работе с массивами и дженериками (например, List). Также его стоит учитывать при выборе структуры данных для твоей программы. Дженерики подвержены стиранию типов. Информация о типе-параметре недоступна во время выполнения программы. В отличие от них, массивы знают и могут использовать информацию о своем типе данных во время выполнения программы. Попытка поместить в массив значение неверного типа приведет к исключению:
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
Из-за того, что между массивами и дженериками есть такая большая разница, у них могут возникнуть проблемы с совместимостью. Прежде всего, ты не можешь создать массив объектов-дженериков или даже просто типизированный массив. Звучит немного непонятно? Давай рассмотрим наглядно. К примеру, ты не сможешь сделать в 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];
   }
}
Но для чего это сделано? Почему создание таких массивов запрещено? Это все — для обеспечения типобезопасности. Если бы компилятор позволял нам создавать такие массивы из объектов-дженериков, мы могли бы заработать кучу проблем. Вот простой пример из книги Джошуа Блоха “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 было бы разрешено, и компилятор бы не ругался. Вот каких дел мы могли бы наворотить в этом случае: В строке 1 мы создаем массив листов List<String>[] stringLists. Наш массив вмещает в себя один List<String>. В строке 2 мы создаем список чисел List<Integer>. В строке 3 мы присваиваем наш массив List<String>[] в переменную Object[] objects. Язык Java позволяет это делать: в массив объектов X можно помещать и объекты X, и объекты всех дочерних классов Х. Соответственно, в массив Objects можно поместить вообще все что угодно. В строке 4 мы подменяем единственный элемент массива objects (List<String>) на список List<Integer>. В результате мы поместили List<Integer> в наш массив, который предназначался только для хранения List<String>! С ошибкой же мы столкнемся только когда код дойдет до строки 5. Во время выполнения программы будет выброшено исключение ClassCastException. Поэтому запрет на создание таких массивов и был введен в язык Java — это позволяет нам избегать подобных ситуаций.

Как можно обойти стирание типов?

Что ж, стирание типов мы изучили. Давай попробуем обмануть систему! :) Задача: У нас есть класс-дженерик TestClass<T>. Нам нужно создать в нем метод createNewT(), который будет создавать и возвращать новый объект типа Т. Но ведь это невозможно сделать, так? Вся информация о типе Т будет стерта во время компиляции, и в процессе работы программы мы не сможем узнать, объект какого именно типа нам нужно создать. На самом деле, есть один хитрый способ. Ты наверняка помнишь, что в Java есть класс Class. Используя его, мы можем получить класс любого нашего объекта:
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
Но вот одна особенность, о которой мы не говорили. В документации Oracle ты увидишь, что класс Class — это дженерик! Стирание типов - 3В документации написано: “Т — это тип класса, моделируемого этим объектом Class”. Если перевести это с языка документации на человеческий, это означает, что классом для объекта Integer.class является не просто Class, а Class<Integer>. Типом объекта 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("Объект секретного класса успешно создан!");
   }
}
А вот как мы используем на практике наше решение:
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);
Благодаря этому мы сохранили информацию о типе-параметре и уберегли ее от стирания. В итоге мы смогли создать объект T! :) На этом сегодняшняя лекция подходит к концу. О стирании типов всегда необходимо помнить при работе с дженериками. Выглядит это дело не очень удобно, но нужно понимать — дженерики не были частью языка Java при его создании. Это позже прикрученная возможность, которая помогает нам создавать типизированные коллекции и отлавливать ошибки на этапе компиляции. В некоторых других языках, где дженерики появлялись с первой версии, стирание типов отсутствует (например, в C#). Впрочем, мы не закончили изучение дженериков! На следующей лекции ты познакомишься с еще несколькими особенностями работы с ними. А пока было бы неплохо решить пару задач! :)
Комментарии (44)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Алексей
Уровень 41
Expert
24 марта 2023, 07:09
Так и не могу понять про эту запись: public static <T> TestClass<T> createAndAdd2Values(Object o1, Object o2) Почему два раза пишем <T>? Нигде нет информации...
Roockie
Уровень 111
Expert
1 апреля 2023, 05:34
Как я понял, первое <T>, это тип принимаемый методом, а TestClass<T> это тип возвращаемого значения. Такая запись не даст тебе засунуть или вернуть разные типы. Взял тип на входе, того же типа вернуть и должен.
Bibzen'
Уровень 16
28 июня 2023, 15:55
Дженерик-параметр, объявленный в классе не относится к статическим методам этого класса, поэтому для статических методов необходимо указывать дженерик-параметры отдельно, после модификатора static. Они у него свои.
Anonymous #3268884
Уровень 35
25 июля 2023, 12:20
это синтаксис так называемого обобщенного метода. Подробно можно покурить в Гугле по теме "обобщенный метод в джава", но если вкратце, то суть в том, что если метод работает с дженериками, например принимает дженерики в качестве аргументов, или возвращает дженерик в качестве значения, в общем работает с дженериками, то в записи этого метода перед типом возвращаемого значения или перед модификатором войд, если метод ничего не возвращает, нужно указать в треугольных скобках, какой именно тип-параметр должны принимать дженерики, с которыми работает данный метод. Например: public <X> void myMethod( AnyClass<X> x ); или: public static <Y> AnyClass<Y> myOtherMethod( AnyClass<Y> y1, AnyClass<Y> y2 );
Gregory Parfer
Уровень 82
Expert
3 января 2023, 13:32
Отличное видео с разъяснениями по этой теме: https://www.youtube.com/watch?v=pgzHglCsTcw
Alexei
Уровень 42
25 июня 2023, 11:26
Ant
Уровень 29
9 декабря 2022, 13:28
Как можно посмотреть стирание типов в коде? написал свой типизированный ArrayList, собрал в .class, и глядя внутрь вижу тот же типизированный класс. Не могу понять, что не так?)
hidden #3155849
Уровень 2
5 ноября 2022, 21:32
Откуда вообще это ваше <T> TestClass<T> Зачем и что такое <T>?
bprint
Уровень 41
8 ноября 2022, 15:27
Думаю для 5-го уровня рановато изучать дженерики.
hidden #3155849
Уровень 2
12 декабря 2022, 21:23
лучше б с ответом помог 😬
Gvrko
Уровень 36
14 января 2023, 15:30
<T> Позволяет работать с объектами любого типа без необходимости предварительного определения типа. Это позволяет использовать один и тот же код для работы с различными типами данных.
velmik
Уровень 38
5 сентября 2022, 08:11
Все больше прихожу к пониманию того, что надо-бы как-то всем сообществам договориться о какой-то одной версии вектора алгоритма - безопасности в определленных сферах используемого языка, также касаемо и обучения программирования. А все остальное отдать(факультативно) уже на откуп для любителей поиска не стандартных решений! в других сферах жизни.
1 сентября 2022, 06:31
Даже когда был сделан упор на объяснение понятным языком, все равно ничего не понял))). Нигде понятнее не нашел, чем в видео Заура Трегулова про Generics'ы
Денис Стёпшин
Уровень 25
11 августа 2023, 04:45
а чего ссылку не оставил ?)
Руслан
Уровень 43
31 мая 2022, 09:39
В этом примере: List<String> strings = new ArrayList<>(); List<Cat> cats = new ArrayList<>(); System.out.println(strings.getClass() == cats.getClass()); == true. Но strings.getClass и cats.getClass будут равны ArrayList. Т.е. мы проверяем, равен ли ArrayList ему же, а не String и Cat. Или я чего-то не понял?
Марат Гарипов
Уровень 108
Expert
24 июня 2022, 20:36
Насколько я понимаю, ArrayList<> = это тип объекта, который присваивается переменной как раз таки класса List с параметрами <String> или <Cat>, и после компиляции программы из-за стирания типов эти параметры и пропадают, а остаются только List'ы, которые, соответственно, равны
Павел
Уровень 92
Expert
2 октября 2023, 08:54
Надо бы вообще представлять что делает getClass(). Может этот метод не предназначен для выяснения типа элементов составляющих, например, ArrayList, а предназначен только для определения класса ArrayList. Поэтому не стоило выдавать результат сравнения true как неожиданность.
Серега Батенин
Уровень 34
3 апреля 2022, 16:17
Как говорит одна моя знакомая "Очень интересно, но нифига не понятно". Вот и мне пока мало что понятно(
Мирослав
Уровень 29
Expert
12 мая 2022, 08:28
4 сентября 2022, 07:00
так говорит не только одна твоя знакомая)
LuneFox Java Developer в BIFIT Expert
26 января 2022, 14:25
Мне кажется, или почти все лекции от профессора заканчиваются фразой "А пока было бы неплохо решить пару задач!", даже если следующей лекцией идёт не решение задач?)
Igor Java/Kotlin Developer
12 июня 2021, 12:17
Я бы пояснение дал в лекции по последнему примеру. Что там всё происходит из-за рефлексии.
Anonymous #3068853
Уровень 3
27 мая 2022, 23:49
А, ну тогда ясно 😆