Какова концепция стирания в дженериках в Java?

Ответы:

203

Это в основном способ реализации дженериков в Java с помощью уловки компилятора. Скомпилированный универсальный код на самом деле просто используется java.lang.Objectвезде, где вы говорите T(или о каком-либо другом параметре типа), и есть некоторые метаданные, чтобы сообщить компилятору, что это действительно универсальный тип.

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

Сравните это, скажем, с C #, где информация сохраняется во время выполнения, позволяя коду содержать выражения, например, typeof(T)которые эквивалентны T.class- за исключением того, что последнее недействительно. (Имейте в виду, что между универсальными шаблонами .NET и универсальными шаблонами Java существуют и другие различия.) Стирание типов является источником многих «странных» предупреждений / сообщений об ошибках при работе с универсальными шаблонами Java.

Другие источники:

Джон Скит
источник
6
@Rogerio: Нет, у объектов не будет разных универсальных типов. В поле известны типов, но объекты не делают.
Джон Скит,
8
@Rogerio: Абсолютно - во время выполнения очень легко узнать, действительно ли то, что предоставляется только как Object(в слабо типизированном сценарии), на самом деле, например, а List<String>). В Java это просто невозможно - вы можете узнать, что это был ArrayList, но не то, что было исходным универсальным типом. Такие вещи могут возникнуть, например, в ситуациях сериализации / десериализации. Другой пример - контейнер должен иметь возможность создавать экземпляры своего универсального типа - вы должны передавать этот тип отдельно в Java (as Class<T>).
Джон Скит,
6
Я никогда не утверждал, что это всегда или почти всегда было проблемой, но, по моему опыту, это, по крайней мере, довольно часто. Есть разные места, где я вынужден добавить Class<T>параметр в конструктор (или общий метод) просто потому, что Java не сохраняет эту информацию. Посмотрите, EnumSet.allOfнапример, - аргумента универсального типа для метода должно быть достаточно; зачем мне указывать еще и «нормальный» аргумент? Ответ: типа стирание. Подобные вещи загрязняют API. Ради интереса, часто ли вы использовали дженерики .NET? (продолжение)
Джон Скит,
5
До того, как я начал использовать дженерики .NET, я находил дженерики Java по-разному неудобными (и использование подстановочных знаков по-прежнему является головной болью, хотя форма дисперсии, определяемая вызывающим абонентом, определенно имеет свои преимущества) - но это произошло только после того, как я использовал дженерики .NET на какое-то время я увидел, сколько шаблонов стало неудобным или невозможным с дженериками Java. Это снова парадокс Блаба. Я не говорю, что у дженериков .NET тоже нет недостатков, кстати - существуют различные отношения типов, которые, к сожалению, не могут быть выражены, - но я предпочитаю их дженерикам Java.
Джон Скит,
5
@Rogerio: С отражением можно многое сделать, но я не считаю, что хочу делать это почти так же часто, как то, что я не могу делать с помощью дженериков Java. Я не хочу выяснять аргумент типа для поля почти так часто, как я хочу выяснять аргумент типа фактического объекта.
Джон Скит,
41

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

javac -XD-printflat -d output_dir SomeFile.java

Это -printflatфлаг, который передается компилятору, который генерирует файлы. ( -XDЧасть - это то, что говорит javacпередать его исполняемому jar-файлу, который на самом деле выполняет компиляцию, а не просто javac, но я отвлекаюсь ...) Это -d output_dirнеобходимо, потому что компилятору нужно место для размещения новых файлов .java.

Это, конечно, больше, чем просто стирание; здесь выполняется вся автоматическая работа компилятора. Например, также вставляются конструкторы по умолчанию, новые forциклы в стиле foreach расширяются до обычных 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();

    }
} 
Parag
источник
Я попытался использовать декомпилятор java, чтобы увидеть код после стирания типа из файла .class, но в файле .class все еще есть информация о типе. Я пробовал jigawotсказал, работает.
Frank
25

Чтобы завершить уже очень полный ответ Джона Скита, вы должны осознать, что концепция стирания типа проистекает из необходимости совместимости с предыдущими версиями Java .

Первоначально представленный на EclipseCon 2007 (больше не доступен), совместимость включала следующие моменты:

  • Совместимость источников (приятно иметь ...)
  • Двоичная совместимость (обязательно!)
  • Совместимость миграции
    • Существующие программы должны продолжать работать
    • Существующие библиотеки должны иметь возможность использовать универсальные типы
    • Должны быть!

Оригинальный ответ:

Отсюда:

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

Есть предложения для большего овеществления . Reify быть «Считайте абстрактное понятие реальным», где языковые конструкции должны быть концепциями, а не просто синтаксическим сахаром.

Я также должен упомянуть checkCollectionметод Java 6, который возвращает динамически безопасное представление указанной коллекции. Любая попытка вставить элемент неправильного типа приведет к немедленному выполнению ClassCastException.

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

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

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

  • когда коллекция передается в стороннюю библиотеку и крайне важно, чтобы код библиотеки не повредил коллекцию, вставив элемент неправильного типа.
  • программа завершается с ошибкой 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();
}
VonC
источник
2
Обратите внимание, что обратная совместимость могла бы быть достигнута без стирания типов, но не без того, чтобы программисты Java изучили новый набор коллекций. Это именно тот путь, по которому пошел .NET. Другими словами, именно этот третий пункт является наиболее важным. (Продолжение.)
Джон Скит
15
Лично я считаю, что это была близорукая ошибка - это давало краткосрочное преимущество и долгосрочное неудобство.
Джон Скит
9

Дополняя уже дополненный ответ Джона Скита ...

Было упомянуто, что реализация дженериков посредством стирания приводит к некоторым досадным ограничениям (например, нет new T[42]). Также упоминалось, что основной причиной такого поведения была обратная совместимость в байт-коде. Это тоже (в основном) правда. Сгенерированный байт-код -target 1.5 несколько отличается от просто лишенного сахара приведения -target 1.4. Технически даже возможно (с помощью огромных уловок) получить доступ к экземплярам универсального типа во время выполнения , доказав, что в байт-коде действительно что-то есть.

Более интересным моментом (который не поднимался) является то, что реализация универсальных шаблонов с использованием стирания предлагает немного больше гибкости в том, что может выполнять система типов высокого уровня. Хорошим примером этого может быть реализация Scala JVM по сравнению с CLR. В JVM можно напрямую реализовать более высокие типы, поскольку сама JVM не накладывает ограничений на универсальные типы (поскольку эти «типы» фактически отсутствуют). Это контрастирует с CLR, которая знает экземпляры параметров во время выполнения. Из-за этого сама среда CLR должна иметь некоторое представление о том, как следует использовать универсальные шаблоны, сводя на нет попытки расширить систему с помощью непредвиденных правил. В результате высшие типы Scala в CLR реализованы с использованием странной формы стирания, эмулируемой в самом компиляторе,

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

Даниэль Спивак
источник
7
Неудобство не в том, что вы хотите делать «непослушные» вещи во время выполнения. Это когда вы хотите делать совершенно разумные вещи во время выполнения. Фактически, стирание типа позволяет вам делать гораздо более непослушные вещи - например, преобразовывать List <String> в List, а затем в List <Date> только с предупреждениями.
Джон Скит,
5

Насколько я понимаю (будучи парнем из .NET ), JVM не имеет понятия дженериков, поэтому компилятор заменяет параметры типа на Object и выполняет за вас все приведение типов.

Это означает, что дженерики Java представляют собой не что иное, как синтаксический сахар и не предлагают никакого улучшения производительности для типов значений, которые требуют упаковки / распаковки при передаче по ссылке.

Эндрю Кеннан
источник
3
Дженерики Java в любом случае не могут представлять типы значений - такого понятия как List <int> не существует. Однако в Java нет передачи по ссылке вообще - она ​​строго передается по значению (где это значение может быть ссылкой)
Джон Скит,
5

Зачем использовать Generices

Вкратце, универсальные типы позволяют типам (классам и интерфейсам) быть параметрами при определении классов, интерфейсов и методов. Подобно более знакомым формальным параметрам, используемым в объявлениях методов, параметры типа предоставляют возможность повторно использовать один и тот же код с разными входными данными. Разница в том, что входными данными для формальных параметров являются значения, а входными данными для параметров типа являются типы. У оды, использующей дженерики, есть много преимуществ по сравнению с обычным кодом:

  • Более строгие проверки типов во время компиляции.
  • Устранение слепков.
  • Предоставление программистам возможности реализовать общие алгоритмы.

Что такое стирание шрифта

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

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

[NB] -Что такое мостовой метод? Вкратце, в случае параметризованного интерфейса, такого как Comparable<T>, это может вызвать добавление дополнительных методов компилятором; эти дополнительные методы называются мостами.

Как работает стирание

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

  • Стирание 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)

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

Надеюсь, Reader понравится :)

Атиф
источник
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);


   }
}
snr
источник