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

238

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

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

Radiodef
источник
2
Более «общая» версия этого вопроса: stackoverflow.com/questions/313584/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@afedingpan: статья, упомянутая в моем ответе, подробно объясняет, как и когда происходит стирание типа. Это также объясняет, когда хранится информация о типе. Другими словами: усовершенствованные дженерики доступны на Java, вопреки распространенному мнению. Смотрите: rgomes.info/using-typetokens-to-retrieve-generic-parameters
Ричард Гомес

Ответы:

240

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

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для самого объекта списка.

Джон Скит
источник
6
Нет, даже при использовании универсального типа во время выполнения могут быть доступны метаданные. Локальная переменная недоступна через Reflection, но для параметра метода, объявленного как «List <String> l», будут метаданные типа во время выполнения, доступные через Reflection API. Да, «стирание типа» не так просто, как думают многие ...
Rogério
4
@Rogerio: Когда я ответил на ваш комментарий, я полагаю, что вы путаетесь между возможностью получить тип переменной и возможностью получить тип объекта . Сам объект не знает своего аргумента типа, хотя поле знает.
Джон Скит
Конечно, просто глядя на сам объект, вы не можете знать, что это List <String>. Но объекты не просто появляются из ниоткуда. Они создаются локально, передаются в качестве аргумента вызова метода, возвращаются как возвращаемое значение из вызова метода или считываются из поля некоторого объекта ... Во всех этих случаях вы МОЖЕТЕ знать во время выполнения, что такое универсальный тип, либо неявно или с помощью Java Reflection API.
Рожерио
13
@Rogerio: Откуда ты знаешь, откуда появился этот объект? Если у вас есть параметр типа, List<? extends InputStream>как вы можете узнать, какой он был при создании? Даже если вы можете узнать тип поля, в котором сохранена ссылка, зачем вам это нужно? Почему вы должны иметь возможность получать всю остальную информацию об объекте во время выполнения, а не его аргументы общего типа? Похоже, вы пытаетесь изобразить стирание типа как крошечную вещь, которая на самом деле не влияет на разработчиков - хотя в некоторых случаях я считаю, что это очень серьезная проблема.
Джон Скит
Но стирание типов - это крошечная вещь, которая на самом деле не влияет на разработчиков! Конечно, я не могу говорить за других, но по моему опыту это никогда не было большим делом. На самом деле я использую информацию о типах времени исполнения при разработке моего Java Mocking API (JMockit); по иронии судьбы API-интерфейсы для .NET, по-видимому, меньше используют преимущества универсальной системы типов, доступной в C #.
Rogério
99

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

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

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

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

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

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

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

https://rgomes.info/using-typetokens-to-retrieve-generic-parameters/

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

Образец кода

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

Ричард Гомес
источник
1
@ will824: я значительно улучшил ответ и добавил ссылку на несколько тестовых случаев. Приветствия :)
Ричард Гомес
1
На самом деле они не поддерживали совместимость как с двоичными файлами, так и с исходными кодами: oracle.com/technetwork/java/javase/compatibility-137462.html Где я могу узнать больше об их намерениях? Документы говорят, что он использует стирание типа, но не говорит, почему.
Дмитрий Лазерка
@Richard Действительно, отличная статья! Можно добавить, что локальные классы тоже работают и что в обоих случаях (анонимные и локальные классы) информация о желаемом аргументе типа сохраняется только в случае прямого доступа, а new Box<String>() {};не в случае косвенного доступа, void foo(T) {...new Box<T>() {};...}поскольку компилятор не сохраняет информацию о типе для объявление метода вложения.
Янн-Гаэль Геенек
Я исправил неработающую ссылку на мою статью. Я медленно очищаю свою жизнь и возвращаю свои данные. :-)
Ричард Гомес
33

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

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

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

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

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

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

Эриксон
источник
2
Фактически, это формальный универсальный тип полей и методов, которые компилируются в класс, т. Е. Типичный «T». Чтобы получить реальный тип универсального типа, вы должны использовать «трюк анонимного класса» .
Янн-Гаэль Геенек
13

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

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

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

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

Евгений Йокота
источник
6

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

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

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

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

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

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

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

Винко Врсалович
источник
6

Термин «стирание типа» на самом деле не является правильным описанием проблемы 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.

iconfly
источник
2

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

porfirion
источник