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

Я прочитал о стирании типа Java на веб-сайте Oracle.

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

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

Рассмотрим следующий пример: Say class 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 с непараметрированным аргументом (raw type): 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<T> по-прежнему объявляет себя универсальным.

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

Ответ 2

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

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

Что касается стирания стилей

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

Что касается обобщенных дженериков в Java

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

Я написал статью по этому вопросу:

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

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

Пример кода

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

Ответ 3

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

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

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

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

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

Часто задаваемые вопросы по Angelika Langer - лучшая ссылка, которую я видел для Java Generics.

Ответ 4

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

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

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

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

Ответ 5

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

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

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

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

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

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

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

Ответ 6

Я столкнулся с типом стирания в 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

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

Ответ 7

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

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

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

type erasure относится к процессу загрузки-времени, с помощью которого аннотации явного типа удаляются из программы, прежде чем они будут выполнены во время выполнения

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

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

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

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

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

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

Из нашей вики

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

Reification означает преобразование чего-то абстрактного (Parametric Type) в нечто конкретное (конкретный тип) по специализации.

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

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

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

представляет собой абстракцию, подробно конструктор типа, который получает "reified", когда специализируется на конкретном типе, например 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.