Стирание типов Java generics: когда и что происходит?
Я прочитал об удалении типов Java на веб-сайте Oracle.
Когда происходит стирание типов? Во время компиляции или выполнения? При загрузке класса? При создании экземпляра класса?
На многих сайтах (включая официальный учебник, упомянутый выше) говорится, что стирание типов происходит во время компиляции. Если информация о типе полностью удаляется во время компиляции, как JDK проверяет совместимость типов, когда вызывается метод, использующий generics, без информации о типе или с неверной информацией о типе?
Рассмотрим следующий пример: допустим, у класса A
есть метод, empty(Box<? extends Number> b)
. Мы компилируем A.java
и получаем файл класса A.class
.
public class A {
public static void empty(Box<? extends Number> b) {}
}
public class Box<T> {}
Теперь мы создаем другой класс, B
который вызывает метод empty
с непараметризованным аргументом (необработанный тип): empty(new Box())
. Если мы скомпилируем B.java
с A.class
в пути к классу, javac достаточно умен, чтобы выдать предупреждение. Таким образом, в нем A.class
хранится некоторая информация о типе.
public class B {
public static void invoke() {
// java: unchecked method invocation:
// method empty in class A is applied to given types
// required: Box<? extends java.lang.Number>
// found: Box
// java: unchecked conversion
// required: Box<? extends java.lang.Number>
// found: Box
A.empty(new Box());
}
}
Я предполагаю, что стирание типов происходит при загрузке класса, но это всего лишь предположение. Итак, когда это происходит?
Переведено автоматически
Ответ 1
Удаление типов применяется к использованию дженериков. В файле класса определенно есть метаданные, которые говорят, является ли метод / тип универсальным, каковы ограничения и т.д. Но когда используются дженерики, они преобразуются в проверки во время компиляции и приведения во время выполнения. Итак, этот код:
List<String> list = new ArrayList<String>();
list.add("Hi");
String x = list.get(0);
компилируется в
List list = new ArrayList();
list.add("Hi");
String x = (String) list.get(0);
Во время выполнения нет способа узнать, что T=String
для объекта list - эта информация исчезла.
... но List<T>
сам интерфейс по-прежнему рекламирует себя как универсальный.
РЕДАКТИРОВАТЬ: Просто для пояснения, компилятор сохраняет информацию о том, что переменная является a List<String>
- но вы по-прежнему не можете узнать это T=String
для самого объекта list .
Ответ 2
Компилятор отвечает за понимание дженериков во время компиляции. Компилятор также отвечает за удаление этого "понимания" универсальных классов в процессе, который мы называем стиранием типов. Все происходит во время компиляции.
Примечание: Вопреки убеждениям большинства разработчиков Java, можно сохранять информацию о типе во время компиляции и извлекать эту информацию во время выполнения, хотя и очень ограниченным способом. Другими словами: Java предоставляет овеществленные дженерики очень ограниченным способом.
Что касается стирания типов
Обратите внимание, что во время компиляции компилятору доступна полная информация о типе, но эта информация намеренно удаляется в общем случае при генерации байт-кода в процессе, известном как стирание типов. Это сделано таким образом из-за проблем с совместимостью: Целью разработчиков языка было обеспечение полной совместимости исходного кода и байт-кода между версиями платформы. Если бы это было реализовано по-другому, вам пришлось бы перекомпилировать ваши устаревшие приложения при переходе на более новые версии платформы. То, как это было сделано, все сигнатуры методов сохранены (совместимость с исходным кодом), и вам не нужно ничего перекомпилировать (двоичная совместимость).
Что касается овеществленных дженериков в Java
Если вам нужно сохранить информацию о типе во время компиляции, вам необходимо использовать анонимные классы. Суть в том, что в очень особом случае анонимных классов можно получить полную информацию о типе во время компиляции во время выполнения, что, другими словами, означает: овеществленные обобщения. Это означает, что компилятор не выбрасывает информацию о типе, когда задействованы анонимные классы; эта информация хранится в сгенерированном двоичном коде, и система выполнения позволяет вам извлекать эту информацию.
Я написал статью на эту тему:
https://rgomes.info/using-typetokens-to-retrieve-generic-parameters/
Примечание о методе, описанном в статье выше, заключается в том, что этот метод непонятен большинству разработчиков. Несмотря на то, что он работает, и работает хорошо, большинство разработчиков чувствуют замешательство или дискомфорт от этого метода. Если у вас общая база кода или вы планируете опубликовать свой код для широкой публики, я не рекомендую описанный выше метод. С другой стороны, если вы являетесь единственным пользователем своего кода, вы можете воспользоваться преимуществами, которые предоставляет вам этот метод.
Пример кода
В статье выше есть ссылки на примеры кода.
Ответ 3
Если у вас есть поле универсального типа, параметры его типа компилируются в класс.
Если у вас есть метод, который принимает или возвращает универсальный тип, параметры этого типа компилируются в класс.
Эта информация используется компилятором, чтобы сообщить вам, что вы не можете передать Box<String>
методу empty(Box<T extends Number>)
.
API сложный, но вы можете проверить информацию об этом типе через reflection API с помощью таких методов, как getGenericParameterTypes
, getGenericReturnType
, и, для полей, getGenericType
.
Если у вас есть код, использующий универсальный тип, компилятор вставляет приведения по мере необходимости (в вызывающем объекте) для проверки типов. Сами универсальные объекты представляют собой просто необработанный тип; параметризованный тип "стирается". Итак, когда вы создаете new Box<Integer>()
, в Integer
объекте нет информации о Box
классе.
Часто задаваемые вопросы Анжелики Лангер - лучший справочник, который я видел по Java Generics.
Ответ 4
Generics на языке Java - действительно хорошее руководство по этой теме.
Дженерики реализуются компилятором Java в виде интерфейсного преобразования, называемого erasure. Вы можете (почти) думать об этом как о переводе от источника к источнику, при котором общая версия
loophole()
преобразуется в не общую версию.
Итак, это происходит во время компиляции. JVM никогда не узнает, какой из них ArrayList
вы использовали.
Я бы также порекомендовал ответ мистера Скита на вопрос, Что такое концепция стирания в дженериках Java?