Какова концепция стирания в 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.
Другие источники:
- Документация Oracle
- Википедия
- Gilad Bracha Java generics guide (PDF - настоятельно рекомендуется; ссылка может потребоваться периодически менять)
- Анжелика Лангер: общие вопросы по 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)