Стирание типа дженериков Java: когда и что происходит?

Я читал об удалении типов 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 в classpath 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 ответов

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

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> Сам интерфейс по-прежнему рекламирует себя как общий.

РЕДАКТИРОВАТЬ: просто чтобы уточнить, компилятор сохраняет информацию о переменной, являющейся List<String> - но вы все еще не можете узнать это T=String для самого объекта списка.

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

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

По поводу стирания типа

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

Относительно усовершенствованных дженериков в Java

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

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

http://rgomes-info.blogspot.co.uk/2013/12/using-typetokens-to-retrieve-generic.html

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

Образец кода

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

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

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

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

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

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

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

Generics in Java Language - действительно хорошее руководство по этой теме.

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

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

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

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

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

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

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

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

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

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

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

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

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

Из нашей вики

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

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

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

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.

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

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

преобразуется в

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

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

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

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

Я столкнулся с стиранием типа в 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, передаваемыми в аргументах функции.

Другие вопросы по тегам