Що таке поняття стирання в генериці на Java?


Відповіді:


200

Це в основному спосіб, що дженерики реалізовані в Java за допомогою хитрості компілятора. Складений загальний код насправді використовується лише java.lang.Objectтам, де ви говоритеT (або якийсь інший параметр типу) - і є деякі метадані, щоб сказати компілятору, що він справді є загальним типом.

Коли ви компілюєте якийсь код із загальним типом чи методом, компілятор визначає, що ви дійсно маєте на увазі (тобто, для чого це аргумент типу T) і під час компіляції перевіряє, що ви робите правильно, але випромінений код знову просто говорить з точки зору java.lang.Object- компілятор створює додаткові касти, де це необхідно. На час виконання, a List<String> і a List<Date>точно однакові; компілятор видалив додаткову інформацію про тип .

Порівняйте це, скажімо, з C #, де інформація зберігається під час виконання, дозволяючи коду містити вирази, такі, typeof(T)що еквівалентноT.class - за винятком того, що останні недійсні. (Існують додаткові відмінності між .NET generics і Java generics, зауважте.) Видалення типу є джерелом багатьох "непарних" повідомлень про попередження / помилки під час роботи з Java generics.

Інші ресурси:


6
@Rogerio: Ні, об’єкти не матимуть різних загальних типів. В поле відомі типів, але об'єкти не роблять.
Джон Скіт

8
@Rogerio: Абсолютно - надзвичайно просто дізнатися під час виконання, чи є щось, наприклад, лише таке, що Object(у слабко набраному сценарії) насправді а List<String>). На Java це просто неможливо - ви можете дізнатися, що це ArrayList, але не те, що було оригінальним родовим типом. Такі речі можуть виникнути, наприклад, у серіалізації / десеріалізації. Іншим прикладом є те, коли контейнер повинен бути в змозі побудувати екземпляри свого загального типу - ви повинні передавати цей тип окремо в Java (як Class<T>).
Джон Скіт

6
Я ніколи не стверджував, що це завжди або майже завжди було проблемою - але, принаймні, досить часто це є моїм досвідом. Є різні місця, де я змушений додавати Class<T>параметр до конструктора (або загального методу) просто тому, що Java не зберігає цю інформацію. Подивіться, EnumSet.allOfнаприклад - аргументу загального типу методу має бути достатньо; чому мені також потрібно вказати "звичайний" аргумент? Відповідь: введіть стирання. Така річ забруднює API. Не цікаво, чи багато ви використовували .NET-дженерики? (продовження)
Джон Скіт

5
Перед тим, як я скористався .NET generics, я виявив, що дженерики Java незграбними різними способами (і макіяж все ще є головним болем, хоча форма "дисперсія, визначена абонентом", безумовно, має переваги) - але це було лише після того, як я використовував .NET generics на деякий час я побачив, як багато моделей стали незручними або неможливими з дженериками Java. Знову парадокс Блуба. Я не кажу, що .NET generics також не має недоліків, btw - є різні відносини типу, які неможливо виразити, на жаль, - але я набагато віддаю перевагу цим дженерікам Java.
Джон Скіт

5
@Rogerio: Є багато що можна зробити з роздумом - але я не схильний знаходити, що хочу робити ці речі майже так само часто, як речі, які я не можу зробити з дженериками Java. Я не хочу з’ясовувати аргумент типу для поля майже так само часто, як я хочу з’ясувати аргумент типу фактичного об’єкта.
Джон Скіт

41

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

javac -XD-printflat -d output_dir SomeFile.java

Це -printflatпрапор, який передається компілятору, який генерує файли. ( -XDЧастина - це те, що говорить javacпро те, щоб передати її виконуваному баночку, який насправді робить компіляцію, а не просто javac, але я відхиляюсь ...) Це -d output_dirнеобхідно, тому що компілятору потрібно десь помістити нові файли .java.

Це, звичайно, робить більше, ніж просто стирання; всі автоматичні речі, які робить компілятор, виконуються тут. Наприклад, також вставляються конструктори за замовчуванням, нові forпетлі у стилі передбачень розширюються на звичайні forпетлі тощо. Приємно бачити дрібниці, які відбуваються автоматично.


29

Видалення, буквально означає, що інформація про тип, яка присутня у вихідному коді, стирається із зібраного байтового коду. Давайте зрозуміємо це з деяким кодом.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class GenericsErasure {
    public static void main(String args[]) {
        List<String> list = new ArrayList<String>();
        list.add("Hello");
        Iterator<String> iter = list.iterator();
        while(iter.hasNext()) {
            String s = iter.next();
            System.out.println(s);
        }
    }
}

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

import java.io.PrintStream;
import java.util.*;

public class GenericsErasure
{

    public GenericsErasure()
    {
    }

    public static void main(String args[])
    {
        List list = new ArrayList();
        list.add("Hello");
        String s;
        for(Iterator iter = list.iterator(); iter.hasNext(); System.out.println(s))
            s = (String)iter.next();

    }
} 

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

25

Щоб виконати і без того дуже повну відповідь Джона Скіта, ви повинні усвідомити концепцію стирання типу, що випливає з потреби сумісності з попередніми версіями Java .

Спочатку представлена ​​на EclipseCon 2007 (більше не доступна), сумісність включала такі моменти:

  • Сумісність з джерелом (Приємно мати ...)
  • Бінарна сумісність (Обов'язково!)
  • Міграційна сумісність
    • Існуючі програми повинні продовжувати працювати
    • Існуючі бібліотеки повинні мати можливість використовувати загальні типи
    • Повинен мати!

Оригінальна відповідь:

Звідси:

new ArrayList<String>() => new ArrayList()

Є пропозиції щодо більшого вдосконалення . Повторіть, що це "Розглядайте абстрактне поняття як реальне", де мовні конструкції повинні бути поняттями, а не лише синтаксичним цукром.

Я також повинен згадати checkCollectionметод Java 6, який повертає динамічно набір безпечного виду зазначеної колекції. Будь-яка спроба вставити елемент неправильного типу призведе до негайного ClassCastException.

Механізм generics на мові забезпечує перевірку типу компіляції (статичний) типів, але можна перемогти цей механізм за допомогою неперевірених закидів .

Зазвичай це не є проблемою, оскільки компілятор видає попередження про всі такі неперевірені операції.

Однак бувають випадки, коли самої перевірки статичного типу недостатньо, наприклад:

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

Оновлення липня 2012 року, майже через чотири роки:

Він зараз (2012 р.) Детально описаний у " Правилах сумісності API міграції (тест підпису) "

Мова програмування Java реалізує дженерики за допомогою стирання, що забезпечує застарілі та загальні версії, як правило, генерують файли однакових класів, за винятком деякої допоміжної інформації про типи. Бінарна сумісність не порушена, оскільки можна замінити застарілий клас класу на загальний файл класу без зміни або перекомпіляції будь-якого коду клієнта.

Для полегшення взаємодії з негенеричним спадковим кодом також можливе використання стирання параметризованого типу як типу. Такий тип називається необробленим типом ( специфікація мови Java 3 / 4.8 ). Дозвіл необробленого типу також забезпечує зворотну сумісність вихідного коду.

Відповідно до цього, наступні версії java.util.Iteratorкласу сумісні як бінарні, так і вихідні коди:

Class java.util.Iterator as it is defined in Java SE version 1.4:

public interface Iterator {
    boolean hasNext();
    Object next();
    void remove();
}

Class java.util.Iterator as it is defined in Java SE version 5.0:

public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}

2
Зауважте, що зворотну сумісність можна було досягти без стирання типів, але не без того, щоб програмісти Java вивчили новий набір колекцій. Саме такий шлях пройшов .NET. Іншими словами, саме ця третя куля є важливою. (Продовження.)
Джон Скіт

15
Особисто я вважаю, що це короткозорі помилки - це дало короткочасну перевагу та довгостроковий недолік.
Джон Скіт

8

Доповнюючи вже доповнену відповідь Джон Скіт ...

Згадувалося, що реалізація генерики за допомогою стирання призводить до деяких дратівливих обмежень (наприклад, ні new T[42]). Також було відзначено, що основною причиною таких дій була зворотна сумісність у байт-коді. Це також (переважно) правда. Генерований байт-код -ціль 1.5 дещо відрізняється від лише задекларованого кастингу -цілі 1.4. Технічно навіть можна (через величезну хитрість) отримати доступ до загальних типів даних під час виконання , довівши, що в байт-коді дійсно є щось.

Більш цікавий момент (який не піднімався) полягає в тому, що реалізація генерики за допомогою стирання пропонує трохи більше гнучкості в тому, що може досягти система високого рівня. Хорошим прикладом цього може бути реалізація JVM Scala проти CLR. У JVM можливо реалізувати вищі типи безпосередньо через те, що сам JVM не встановлює обмежень на загальні типи (оскільки ці "типи" фактично відсутні). Це контрастує з CLR, який має знання часу виконання параметрів. Через це сам CLR повинен мати певне поняття, як слід використовувати дженерики, анулюючи спроби розширення системи непередбачуваними правилами. Як результат, вищі типи Scala в CLR реалізуються за допомогою дивної форми стирання, емульованої в самому компіляторі,

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


6
Незручність полягає не тоді, коли ви хочете робити "неслухняні" речі під час виконання. Це коли ви хочете робити абсолютно розумні речі під час виконання. Насправді стирання типу дозволяє робити набагато приємніші речі - наприклад, викидання списку <String> до списку, а потім до списку <дата> лише з попередженнями.
Джон Скіт

5

Як я це розумію (будучи хлопцем .NET ), JVM не має поняття generics, тому компілятор замінює параметри типу на Object і виконує весь кастинг для вас.

Це означає, що дженерики Java - це не що інше, як синтаксичний цукор, і не пропонують поліпшення продуктивності для типів значень, які потребують боксу / розпакування при передачі посилання.


3
Java-дженерики ніяк не можуть представляти типи значень - немає такого поняття, як Список <int>. Однак у Java взагалі немає прохідних посилань - це суворо пройдене значення (де це значення може бути посиланням.)
Джон Скіт

2

Є хороші пояснення. Я лише додаю приклад, щоб показати, як стирання типу працює з декомпілятором.

Оригінальний клас,

import java.util.ArrayList;
import java.util.List;


public class S<T> {

    T obj; 

    S(T o) {
        obj = o;
    }

    T getob() {
        return obj;
    }

    public static void main(String args[]) {
        List<String> list = new ArrayList<>();
        list.add("Hello");

        // for-each
        for(String s : list) {
            String temp = s;
            System.out.println(temp);
        }

        // stream
        list.forEach(System.out::println);
    }
}

Декомпільований код з його байтового коду,

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
import java.util.function.Consumer;

public class S {

   Object obj;


   S(Object var1) {
      this.obj = var1;
   }

   Object getob() {
      return this.obj;
   }

   public static void main(String[] var0) {

   ArrayList var1 = new ArrayList();
   var1.add("Hello");


   // for-each
   Iterator iterator = var1.iterator();

   while (iterator.hasNext()) {
         String string;
         String string2 = string = (String)iterator.next();
         System.out.println(string2);
   }


   // stream
   PrintStream printStream = System.out;
   Objects.requireNonNull(printStream);
   var1.forEach(printStream::println);


   }
}

2

Навіщо використовувати Generices

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

  • Сильніші перевірки типу під час компіляції.
  • Усунення закидів.
  • Дозволяє програмістам реалізувати загальні алгоритми.

Що таке стирання типу

Generics були представлені на мові Java для забезпечення більш чітких перевірок типу під час компіляції та для підтримки загального програмування. Для реалізації дженерики компілятор Java застосовує стирання типу для:

  • Замініть всі параметри типу на загальні типи їх межами або Об'єктом, якщо параметри типу не обмежені. Отже, створений байт-код містить лише звичайні класи, інтерфейси та методи.
  • Якщо потрібно, вставте роли типів, щоб зберегти безпеку типу.
  • Створити мостові методи для збереження поліморфізму в розширених родових типах.

[NB] -Що таке метод мостів? Коротше: у випадку параметризованого інтерфейсу, такого як Comparable<T>, це може спричинити додавання компілятора додаткових методів; ці додаткові методи називаються мостами.

Як працює стирання

Стирання типу визначається наступним чином: видаліть усі параметри типу з параметризованих типів і замініть будь-яку змінну типу на стирання її зв'язаної, або на Object, якщо вона не має меж, або зі стиранням крайньої лівої межі, якщо вона має кілька меж. Ось кілька прикладів:

  • Стирання List<Integer>, List<String>і List<List<String>>єList .
  • Стирання List<Integer>[]єList[] .
  • Стирання с List само по собі, як і для будь-якого сирого типу.
  • Стирання int саме по собі, подібно до будь-якого примітивного типу.
  • Стирання с Integer само по собі, подібно до будь-якого типу без параметрів типу.
  • Стирання Tу визначенні asListє Object, тому щоT не має меж.
  • Стирання Tу визначенні maxє Comparable, тому T що зв'язанеComparable<? super T> .
  • У Tостаточному визначенні стирання maxє Object, тому Tщо обмежило Object&, Comparable<T>а ми беремо стирання крайньої лівої межі.

Потрібно бути обережними при використанні дженериків

У Java два різні методи не можуть мати однаковий підпис. Оскільки дженерики реалізовані шляхом стирання, також випливає, що два різних методу не можуть мати підписи з одним стиранням. Клас не може перевантажувати два методи, підписи яких мають одне і те ж стирання, і клас не може реалізувати два інтерфейси, які мають одне і те ж стирання.

    class Overloaded2 {
        // compile-time error, cannot overload two methods with same erasure
        public static boolean allZero(List<Integer> ints) {
            for (int i : ints) if (i != 0) return false;
            return true;
        }
        public static boolean allZero(List<String> strings) {
            for (String s : strings) if (s.length() != 0) return false;
            return true;
        }
    }

Ми плануємо, щоб цей код працював так:

assert allZero(Arrays.asList(0,0,0));
assert allZero(Arrays.asList("","",""));

Однак у цьому випадку стирання підписів обох методів однакові:

boolean allZero(List)

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

Сподіваємось, читач сподобається :)

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