Java generics типу стирання: коли і що трапляється?


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());
    }
}

Я думаю, що стирання типу відбувається при завантаженні класу, але це лише здогадка. То коли це відбувається?


2
Більш «загальна» версія цього питання: stackoverflow.com/questions/313584 / ...
Чіро Сантіллі郝海东冠状病六四事件法轮功

@afryingpan: Стаття, згадана у моїй відповіді, докладно пояснює, як і коли відбувається стирання типу. Він також пояснює, коли зберігається інформація про тип. Іншими словами: перероблені дженерики доступні на 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> інтерфейс все ще рекламує себе як загальний.

EDIT: Просто для уточнення, компілятор зберігає інформацію про змінну, яка є a List<String>- але ви все ще не можете дізнатися це T=Stringдля самого об'єкта списку.


6
Ні, навіть при використанні загального типу можуть бути метадані, доступні під час виконання. Локальна змінна недоступна через Reflection, але для параметра методу, оголошеного як "Список <String> l", метадані будуть вводитись під час виконання, доступними через API Reflection. Так, "видалити стирання" не так просто, як багато хто думає ...
Rogério

4
@Rogerio: Коли я відповів на ваш коментар, я вважаю, що ви плутаєтесь між тим, що можете отримати тип змінної та мати можливість отримати тип об'єкта . Сам об’єкт не знає аргументу типу, навіть якщо це поле.
Джон Скіт

Звичайно, дивлячись на сам об’єкт, ви не можете знати, що це Список <String>. Але об’єкти не просто з’являються з нізвідки. Вони створюються локально, передаються як аргумент виклику методу, повертаються як повернене значення з виклику методу або зчитуються з поля якогось об'єкта ... У всіх цих випадках ви МОЖнете знати під час виконання, що таке загальний тип неявно або за допомогою API відбиття Java.
Rogério

13
@Rogerio: Звідки ти знаєш, звідки взявся об’єкт? Якщо у вас є параметр типу, List<? extends InputStream>як ви можете знати, який тип він був, коли він був створений? Навіть якщо ви можете дізнатися тип поля, в якому зберігається посилання, навіщо це робити? Чому ви повинні мати можливість отримувати всю решту інформації про об’єкт під час виконання, але не його загальні аргументи типу? Ви, здається, намагаєтеся зробити стирання типу такою крихітною річчю, яка не впливає на розробників насправді - тоді як я вважаю це дуже важливою проблемою в деяких випадках.
Джон Скіт

Але тип стирання - це крихітна річ, яка насправді не впливає на розробників! Звичайно, я не можу говорити за інших, але в моєму досвіді це ніколи не було великою справою. Я фактично користуюсь інформацією про тип виконання під час створення мого API глузування Java (JMockit); За іронією долі, API знущаються .NET, здається, менше використовують переваги системи загального типу, доступної в C #.
Rogério

99

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

Примітка: На відміну від думок більшості розробників Java, можна зберігати інформацію про тип компіляції та отримувати цю інформацію під час виконання, незважаючи на дуже обмежений спосіб. Іншими словами: Java надає вдосконалені дженерики дуже обмеженим чином .

Щодо стирання типу

Зверніть увагу , що під час компіляції, компілятор має повну інформацію про типі доступна , але ця інформація навмисно впала в цілому , коли генерується байт - код, в процесі , відомому як тип стирання . Це робиться таким чином через проблеми сумісності: Намір мовних дизайнерів забезпечував повну сумісність вихідного коду та повну сумісність байтового коду між версіями платформи. Якби це було реалізовано інакше, вам доведеться перекомпілювати застарілі програми під час переходу на новіші версії платформи. Як це було зроблено, всі підписи методу зберігаються (сумісність вихідного коду), і вам не потрібно нічого перекомпілювати (бінарна сумісність).

Щодо рефікованих дженериків на Java

Якщо вам потрібно зберігати інформацію про тип компіляції, вам потрібно використовувати анонімні класи. Справа полягає в тому, що в особливому випадку анонімних класів можна отримати повну інформацію про тип компіляції під час виконання, що, іншими словами, означає: перероблена загальна інформація. Це означає, що компілятор не викидає інформацію про тип під час залучення анонімних класів; ця інформація зберігається в створеному двійковому коді, і система виконання дозволяє вам отримати цю інформацію.

Я написав статтю на цю тему:

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>() {};...}оскільки компілятор не зберігає інформацію про тип для декларування методу додавання.
Yann-Gaël Guéhéneuc

Я виправив непрацюючу посилання на свою статтю. Я повільно дегулюю своє життя і відновлюю свої дані. :-)
Річард Гомес

33

Якщо у вас є поле загального типу, його параметри типу компілюються в клас.

Якщо у вас є метод, який приймає або повертає загальний тип, ці параметри типу компілюються в клас.

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

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

Якщо у вас є код, який використовує загальний тип, компілятор вставляє касти за потребою (у абонента) для перевірки типів. Самі родові об’єкти - це лише сировинний тип; параметризований тип "стирається". Отже, коли ви створюєте a new Box<Integer>(), немає інформації про Integerклас в Boxоб'єкті.

FAQ щодо Angelika Langer - найкраща посилання, яку я бачив для Java Generics.


2
Власне, це формальний загальний тип полів і методів, що складаються в клас, тобто типовий "T". Щоб отримати реальний тип загального типу, ви повинні використовувати "трюк анонімного класу" .
Yann-Gaël Guéhéneuc

13

Загальна мова на мові Java - це справді хороший посібник з цієї теми.

Генеричні засоби реалізовані компілятором 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<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

Я стикався з стиранням типу в Android. У виробництві ми використовуємо градел з мініфікованим варіантом. Після мінімізації я отримав фатальний виняток. Я створив просту функцію, щоб показати ланцюжок успадкування свого об'єкта:

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.