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

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

Ответ 1

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

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

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

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

Ответ 2

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

javac -XD-printflat -d output_dir SomeFile.java

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

Это, конечно, больше, чем просто стирание; все автоматические вещи, которые делает компилятор, делается здесь. Например, добавлены конструкторы по умолчанию, новые циклы for в стиле foreach расширены до обычных циклов for и т.д. Приятно видеть, что малые вещи происходят автоматически.

Ответ 3

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

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

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

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

Следовательно:

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

Есть предложения для большего reification. Reify "Рассматривать абстрактное понятие как реальное", где языковые конструкции должны быть понятиями, а не только синтаксическим сахаром.

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

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

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

Однако есть моменты, когда проверка статического типа недостаточно, например:

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

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

Теперь (2012) подробно описано в разделе Правила совместимости с API-интерфейсом (тест подписи)"

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

Чтобы облегчить взаимодействие с неэквивалентным устаревшим кодом, также возможно использовать стирание параметризованного типа в качестве типа. Такой тип называется raw type (Спецификация языка 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();
}

Ответ 4

Erasure, буквально означает, что информация о типе, которая присутствует в исходном коде, удаляется из скомпилированного байт-кода. Давайте поразмыслим с этим кодом.

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

    }
} 

Ответ 5

В дополнение к уже дополненному ответу Джона Скита...

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

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

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

Ответ 6

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

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

Ответ 7

Есть хорошие объяснения. Я только добавляю пример, чтобы показать, как стирание типов работает с декомпилятором.

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

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


   }
}

Ответ 8

В версии java 1.5 вводится общее программирование
Прежде всего, что является общим в java?
Общее программирование - это безопасный объект типа. Перед общим набором в коллекции мы можем хранить любой тип объекта. и после генерации мы должны хранить данные определенного типа объекта.

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

Пример. GenericClassDemo

genericclassDemo = новый GenericClassDemo (Employee.java)