Стирание типа дженериков Java: когда и что происходит? - PullRequest
226 голосов
/ 04 декабря 2008

Я читал об удалении типа Java на сайте Oracle .

Когда происходит стирание типа? Во время компиляции или во время выполнения? Когда класс загружается? Когда создается экземпляр класса?

Многие сайты (включая упомянутое выше официальное руководство) говорят, что стирание типа происходит во время компиляции. Если информация о типе полностью удаляется во время компиляции, как JDK проверяет совместимость типов, когда вызывается метод, использующий обобщения, без информации о типе или неправильной информации о типе?

Рассмотрим следующий пример: скажем, у класса 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());
    }
}

Полагаю, стирание типов происходит при загрузке класса, но это всего лишь предположение. Так когда же это случится?

Ответы [ 7 ]

228 голосов
/ 04 декабря 2008

Тип стирания относится к использованию генериков. В файле класса определенно есть метаданные, чтобы сказать, является ли метод / тип универсальным , каковы ограничения и т. Д. Но когда обобщенные элементы используются , они преобразуются в компиляцию время проверки и время выполнения. Итак, этот код:

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<T> по-прежнему объявляет себя универсальным.

EDIT: просто чтобы уточнить, компилятор сохраняет информацию о том, что переменная является List<String> - но вы все равно не можете найти это T=String для самого объекта списка.

90 голосов
/ 04 января 2011

Компилятор отвечает за понимание Generics во время компиляции. Компилятор также отвечает за отбрасывание этого «понимания» универсальных классов в процессе, который мы называем стирание типа . Все происходит во время компиляции.

Примечание: Вопреки убеждениям большинства разработчиков Java, можно хранить информацию о типе во время компиляции и извлекать эту информацию во время выполнения, несмотря на то, что она очень ограничена. Другими словами: Java предоставляет расширенные дженерики очень ограниченным образом .

Относительно стирания типа

Обратите внимание, что во время компиляции у компилятора имеется полная информация о типе, но эта информация намеренно отбрасывается в общем при генерации байтового кода в процессе, известном как стирание типа . Это сделано из-за проблем совместимости: целью разработчиков языка было обеспечение полной совместимости исходного кода и полной байтового кода между версиями платформы. Если бы это было реализовано по-другому, вам пришлось бы перекомпилировать ваши устаревшие приложения при переходе на более новые версии платформы. Таким образом, все сигнатуры методов сохраняются (совместимость с исходным кодом), и вам не нужно ничего перекомпилировать (двоичная совместимость).

Относительно улучшенных обобщений в Java

Если вам нужно хранить информацию о типе во время компиляции, вам нужно использовать анонимные классы. Дело в том, что в очень особом случае анонимных классов можно получить полную информацию о типе времени компиляции во время выполнения, что, другими словами, означает: reified generics. Это означает, что компилятор не выбрасывает информацию о типе, когда задействованы анонимные классы; эта информация хранится в сгенерированном двоичном коде, и система времени выполнения позволяет вам получать эту информацию.

Я написал статью на эту тему:

http://rgomes -info.blogspot.co.uk / 2013/12 / с использованием-typetokens к извлечению-generic.html

Примечание о методике, описанной в статье выше, заключается в том, что метод неясен для большинства разработчиков. Несмотря на то, что это работает и работает хорошо, большинство разработчиков чувствуют смущение или неудобство с техникой. Если у вас есть общая кодовая база или вы планируете опубликовать свой код для общественности, я не рекомендую описанную выше технику. С другой стороны, если вы являетесь единственным пользователем своего кода, вы можете воспользоваться преимуществами этой технологии.

Пример кода

В статье выше есть ссылки на пример кода.

33 голосов
/ 04 декабря 2008

Если у вас есть поле универсального типа, параметры его типа компилируются в класс.

Если у вас есть метод, который принимает или возвращает универсальный тип, эти параметры типа компилируются в класс.

Эта информация используется компилятором, чтобы сообщить вам, что вы не можете передать Box<String> методу empty(Box<T extends Number>).

API сложен, но вы можете проверить информацию этого типа через API отражения с помощью таких методов, как getGenericParameterTypes, getGenericReturnType, а для полей getGenericType.

Если у вас есть код, который использует универсальный тип, компилятор вставляет приведенные значения при необходимости (в вызывающую программу) для проверки типов. Сами универсальные объекты являются просто необработанным типом; параметризованный тип "стерт". Поэтому при создании new Box<Integer>() информация о классе Integer отсутствует в объекте Box.

FAQ Анжелики Лангер - лучший справочник, который я видел для Java Generics.

13 голосов
/ 04 декабря 2008

Обобщения в языке Java - действительно хорошее руководство по этой теме.

Дженерики реализованы на Java компилятор как интерфейсное преобразование называется стирание. Вы можете (почти) думать об этом как источник к источнику перевод, в результате чего общий версия loophole() конвертируется в неуниверсальная версия.

Итак, это во время компиляции. JVM никогда не узнает, какой ArrayList вы использовали.

Я также рекомендовал бы ответ г-на Скита по Какова концепция стирания в дженериках в Java?

6 голосов
/ 04 декабря 2008

Тип стирания происходит во время компиляции. Что означает стирание типа, так это то, что он забудет об общем типе, а не о каждом типе. Кроме того, все еще будут метаданные о типах, являющихся общими. Например

Box<String> b = new Box<String>();
String x = b.getDefault();

конвертируется в

Box b = new Box();
String x = (String) b.getDefault();

во время компиляции. Вы можете получать предупреждения не потому, что компилятор знает, какой тип является родовым, а наоборот, потому что он не знает достаточно, поэтому он не может гарантировать безопасность типов.

Кроме того, компилятор сохраняет информацию о типе параметров в вызове метода, которую вы можете получить с помощью отражения.

Это руководство - лучшее, что я нашел по этому вопросу.

4 голосов
/ 31 июля 2018

Термин «стирание типа» на самом деле не является правильным описанием проблемы Java с обобщениями. Стирание типов само по себе не является плохой вещью, оно очень необходимо для производительности и часто используется в нескольких языках, таких как C ++, Haskell, D.

Прежде чем вы испытаете отвращение, пожалуйста, вспомните правильное определение стирания типа из Wiki

Что такое стирание типа?

стирание типов относится к процессу загрузки, посредством которого явные аннотации типов удаляются из программы перед ее выполнением во время выполнения

Стирание типа означает отбрасывание тегов типа, созданных во время разработки, или предполагаемых тегов типа во время компиляции, так что скомпилированная программа в двоичном коде не содержит тегов типа. И это относится к каждому языку программирования, компилируемому в двоичный код, за исключением некоторых случаев, когда вам нужны теги времени выполнения. Эти исключения включают в себя, например, все экзистенциальные типы (ссылочные типы Java, которые являются подтипами, любой тип во многих языках, объединенные типы). Причиной стирания типов является то, что программы преобразуются в язык, который в некотором роде не типизирован (двоичный язык допускает только биты), поскольку типы являются только абстракциями и утверждают структуру своих значений и соответствующую семантику для их обработки.

Так что это взамен, нормальная естественная вещь.

Проблема Java в другом и вызвана тем, как она преобразуется.

Часто высказываемые утверждения о Java не имеют обобщенных обобщений, что также неверно.

Java исправляется, но неправильно из-за обратной совместимости.

Что такое овеществление?

Из нашей Wiki

Реификация - это процесс, с помощью которого абстрактное представление о компьютерной программе превращается в явную модель данных или другой объект, созданный на языке программирования.

Реификация означает преобразование чего-то абстрактного (Параметрический тип) в нечто конкретное (Конкретный тип) по специализации.

Мы проиллюстрируем это на простом примере:

ArrayList с определением:

ArrayList<T>
{
    T[] elems;
    ...//methods
}

- это абстракция, в деталях конструктор типа, который становится «утонченным», когда специализируется на конкретном типе, скажем, Integer:

ArrayList<Integer>
{
    Integer[] elems;
}

где ArrayList<Integer> действительно тип.

Но это точно то, чего не делает Java !!! , вместо этого они постоянно связывают абстрактные типы со своими границами, то есть производят один и тот же конкретный тип независимо от передаваемых параметров по специальности:

ArrayList
{
    Object[] elems;
}

, который здесь связан с неявным связанным объектом (ArrayList<T extends Object> == ArrayList<T>).

Несмотря на это, он делает непригодными универсальные массивы и вызывает странные ошибки для необработанных типов:

List<String> l= List.<String>of("h","s");
List lRaw=l
l.add(new Object())
String s=l.get(2) //Cast Exception

это вызывает много неясностей, как

void function(ArrayList<Integer> list){}
void function(ArrayList<Float> list){}
void function(ArrayList<String> list){}

относится к той же функции:

void function(ArrayList list)

и, следовательно, перегрузка универсального метода не может использоваться в Java.

2 голосов
/ 14 октября 2016

Я столкнулся с стиранием типов в Android. В производстве мы используем gradle с опцией minify. После минификации у меня есть роковое исключение. Я сделал простую функцию, чтобы показать цепочку наследования моего объекта:

public static void printSuperclasses(Class clazz) {
    Type superClass = clazz.getGenericSuperclass();

    Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
    Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));

    while (superClass != null && clazz != null) {
        clazz = clazz.getSuperclass();
        superClass = clazz.getGenericSuperclass();

        Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
        Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));
    }
}

И есть два результата этой функции:

Не минимизированный код:

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: com.example.App.SortedListWrapper<com.example.App.Models.User>

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: android.support.v7.util.SortedList$Callback<T>

D/Reflection: this class: android.support.v7.util.SortedList$Callback
D/Reflection: superClass: class java.lang.Object

D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null

Сокращенный код:

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: class com.example.App.SortedListWrapper

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: class android.support.v7.g.e

D/Reflection: this class: android.support.v7.g.e
D/Reflection: superClass: class java.lang.Object

D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null

Таким образом, в минимизированном коде фактические параметризованные классы заменяются необработанными типами классов без какой-либо информации о типах. В качестве решения для моего проекта я удалил все вызовы рефлексии и заменил их явными типами params, переданными в аргументах функции.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...