Какова концепция стирания в дженериках в Java?
Какова концепция стирания в дженериках в Java?
11 ответов
Это в основном способ, которым дженерики реализуются в Java с помощью хитрости компилятора. Скомпилированный универсальный код на самом деле просто использует java.lang.Object
где бы вы ни говорили T
(или некоторый другой параметр типа) - и есть некоторые метаданные, чтобы сообщить компилятору, что это действительно универсальный тип.
Когда вы компилируете некоторый код для универсального типа или метода, компилятор понимает, что вы действительно имеете в виду (то есть, для чего используется аргумент типа T
есть) и проверяет во время компиляции, что вы делаете правильные вещи, но выданный код снова просто говорит с точки зрения java.lang.Object
- компилятор генерирует дополнительные приведения, где это необходимо. Во время выполнения List<String>
и List<Date>
точно такие же; дополнительная информация о типе была стерта компилятором.
Сравните это, скажем, с C#, где информация сохраняется во время выполнения, позволяя коду содержать выражения, такие как typeof(T)
что эквивалентно T.class
- за исключением того, что последний является недействительным. (Заметим, что между обобщениями.NET и обобщениями Java есть и другие различия.) Стирание типов является источником многих "странных" предупреждений / сообщений об ошибках при работе с обобщениями Java.
Другие источники:
- Документация Oracle
- Википедия
- Java-руководство Джилада Брачи (PDF - настоятельно рекомендуется; возможно, периодически требуется изменение ссылки)
- Часто задаваемые вопросы по Java Generics от Angelika Langer
Как примечание, это интересное упражнение, чтобы на самом деле увидеть, что делает компилятор, когда он выполняет стирание - делает концепцию немного легче понять. Существует специальный флаг, который вы можете передать компилятору для вывода java-файлов, у которых были удалены общие и вставленные приведения. Пример:
javac -XD-printflat -d output_dir SomeFile.java
-printflat
это флаг, который передается компилятору, который генерирует файлы. (The -XD
часть это то, что говорит javac
передать его исполняемому банку, который на самом деле выполняет компиляцию, а не просто javac
но я отвлекся...) -d output_dir
необходимо, потому что компилятору нужно место для размещения новых файлов.java.
Это, конечно, больше, чем просто стирание; все автоматические вещи, которые делает компилятор, выполняются здесь. Например, конструкторы по умолчанию также вставляются, новый стиль foreach for
петли расширены до регулярных for
петли и т. д. Приятно видеть мелочи, которые происходят автоматически.
Стирание буквально означает, что информация о типе, присутствующая в исходном коде, стирается из скомпилированного байт-кода. Позвольте нам понять это с помощью некоторого кода.
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.
Первоначально представленный на 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();
}
Дополняя уже дополненный ответ Джона Скита...
Было упомянуто, что реализация дженериков посредством стирания приводит к некоторым раздражающим ограничениям (например, нет new T[42]
). Также было упомянуто, что основной причиной таких действий была обратная совместимость в байт-коде. Это также (в основном) правда. Сгенерированный байт-код -target 1.5 несколько отличается от просто обессахаренного приведения -target 1.4. Технически, даже возможно (посредством огромного обмана) получить доступ к экземплярам универсального типа во время выполнения, доказывая, что в байт-коде действительно что-то есть.
Более интересным моментом (который не был поднят) является то, что реализация обобщений с использованием стирания предлагает немного больше гибкости в том, что может достичь система типов высокого уровня. Хороший пример этого - реализация JVM в Scala против CLR. На JVM возможно реализовать более высокие виды напрямую, потому что сама JVM не накладывает ограничений на универсальные типы (так как эти "типы" фактически отсутствуют). Это контрастирует с CLR, который знает во время выполнения об экземплярах параметров. Из-за этого у CLR должна быть некоторая концепция того, как следует использовать дженерики, сводя на нет попытки расширить систему непредвиденными правилами. В результате высшие виды Scala в CLR реализованы с использованием странной формы стирания, эмулируемой внутри самого компилятора, что делает их не полностью совместимыми с простыми старыми обобщениями.NET.
Стирание может быть неудобным, когда вы хотите делать непослушные вещи во время выполнения, но оно предлагает наибольшую гибкость авторам компилятора. Я предполагаю, что это часть того, почему это не исчезнет в ближайшее время.
Насколько я понимаю (будучи парнем .NET), у JVM нет понятия обобщения, поэтому компилятор заменяет параметры типа на Object и выполняет все приведение за вас.
Это означает, что обобщения Java - это не что иное, как синтаксический сахар, и они не обеспечивают какого-либо улучшения производительности для типов значений, которые требуют упаковки / распаковки при передаче по ссылке.
Есть хорошие объяснения. Я только добавляю пример, чтобы показать, как стирание типов работает с декомпилятором.
Оригинальный класс,
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);
}
}
Зачем использовать 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 понравится:)
Подводя итог предыдущим ответам:
В общем, вот как работает стирание. Когда ваш Java-код компилируется, вся информация о типовых типах удаляется (стирается). Это означает замену параметров типа их связанным типом, который **Object**
если не указана явная граница, а затем применяется соответствующее приведение (как определено аргументами типа) для поддержания совместимости типов с типами, указанными аргументами типа. Компилятор также обеспечивает совместимость этого типа.
Такой подход к генерикам означает, что во время выполнения не существует параметров типа. Они просто механизм исходного кода.
Хотя этому вопросу более 12 лет, я рекомендую любому разработчику, желающему увидеть полную картину (что такое стирание и почему это так), внимательно прочитать объяснения Брайана Гетца .
Это также прольет свет на дебаты, которые имели место в комментариях к принятому ответу.
Ваше здоровье
Общее программирование вводится в версии Java 1.5
Прежде всего, что является общим в Java?
Универсальное программирование - это типобезопасный объект. Перед общим в коллекции мы можем хранить любой тип объекта. и после универсального мы должны хранить данные определенного типа объекта.
В чем преимущества дженерика?
Основными преимуществами generic является то, что приведение типов не требуется, а также type-sage и Generic проверят время компиляции. и общий синтаксис Generic это ClassOrInterface, здесь тип это сигнал, что этот класс может установить класс, когда он создан
Пример. GenericClassDemo
genericclassDemo = new GenericClassDemo (Employee.java)