Какова концепция стирания в дженериках в Java?
Ответы:
Это в основном способ реализации дженериков в Java с помощью уловки компилятора. Скомпилированный универсальный код на самом деле просто используется java.lang.Object
везде, где вы говорите T
(или о каком-либо другом параметре типа), и есть некоторые метаданные, чтобы сообщить компилятору, что это действительно универсальный тип.
Когда вы компилируете код для общего типа или метода, компилятор определяет, что вы на самом деле имеете в виду (то есть, для чего нужен аргумент типа T
), и проверяет во время компиляции, что вы делаете правильные вещи, но испущенный код снова просто говорит с точки зрения java.lang.Object
- компилятор при необходимости генерирует дополнительные приведения. Во время выполнения a List<String>
и a List<Date>
полностью совпадают; дополнительная информация о типе была удалена компилятором.
Сравните это, скажем, с C #, где информация сохраняется во время выполнения, позволяя коду содержать выражения, например, typeof(T)
которые эквивалентны T.class
- за исключением того, что последнее недействительно. (Имейте в виду, что между универсальными шаблонами .NET и универсальными шаблонами Java существуют и другие различия.) Стирание типов является источником многих «странных» предупреждений / сообщений об ошибках при работе с универсальными шаблонами Java.
Другие источники:
Object
(в слабо типизированном сценарии), на самом деле, например, а List<String>
). В Java это просто невозможно - вы можете узнать, что это был ArrayList
, но не то, что было исходным универсальным типом. Такие вещи могут возникнуть, например, в ситуациях сериализации / десериализации. Другой пример - контейнер должен иметь возможность создавать экземпляры своего универсального типа - вы должны передавать этот тип отдельно в Java (as Class<T>
).
Class<T>
параметр в конструктор (или общий метод) просто потому, что Java не сохраняет эту информацию. Посмотрите, EnumSet.allOf
например, - аргумента универсального типа для метода должно быть достаточно; зачем мне указывать еще и «нормальный» аргумент? Ответ: типа стирание. Подобные вещи загрязняют API. Ради интереса, часто ли вы использовали дженерики .NET? (продолжение)
В качестве примечания: это интересное упражнение, чтобы на самом деле увидеть, что делает компилятор, когда он выполняет стирание, - это немного упрощает понимание всей концепции. Существует специальный флаг, который вы можете передать компилятору для вывода файлов java, в которых были удалены общие шаблоны и вставлены приведенные типы. Пример:
javac -XD-printflat -d output_dir SomeFile.java
Это -printflat
флаг, который передается компилятору, который генерирует файлы. ( -XD
Часть - это то, что говорит javac
передать его исполняемому jar-файлу, который на самом деле выполняет компиляцию, а не просто javac
, но я отвлекаюсь ...) Это -d output_dir
необходимо, потому что компилятору нужно место для размещения новых файлов .java.
Это, конечно, больше, чем просто стирание; здесь выполняется вся автоматическая работа компилятора. Например, также вставляются конструкторы по умолчанию, новые for
циклы в стиле foreach расширяются до обычных 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();
}
}
jigawot
сказал, работает.
Чтобы завершить уже очень полный ответ Джона Скита, вы должны осознать, что концепция стирания типа проистекает из необходимости совместимости с предыдущими версиями 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. Технически даже возможно (с помощью огромных уловок) получить доступ к экземплярам универсального типа во время выполнения , доказав, что в байт-коде действительно что-то есть.
Более интересным моментом (который не поднимался) является то, что реализация универсальных шаблонов с использованием стирания предлагает немного больше гибкости в том, что может выполнять система типов высокого уровня. Хорошим примером этого может быть реализация Scala JVM по сравнению с CLR. В JVM можно напрямую реализовать более высокие типы, поскольку сама JVM не накладывает ограничений на универсальные типы (поскольку эти «типы» фактически отсутствуют). Это контрастирует с CLR, которая знает экземпляры параметров во время выполнения. Из-за этого сама среда CLR должна иметь некоторое представление о том, как следует использовать универсальные шаблоны, сводя на нет попытки расширить систему с помощью непредвиденных правил. В результате высшие типы Scala в CLR реализованы с использованием странной формы стирания, эмулируемой в самом компиляторе,
Стирание может быть неудобным, когда вы хотите делать непослушные вещи во время выполнения, но оно предлагает максимальную гибкость для авторов компилятора. Я предполагаю, что это одна из причин, почему он не исчезнет в ближайшее время.
Насколько я понимаю (будучи парнем из .NET ), JVM не имеет понятия дженериков, поэтому компилятор заменяет параметры типа на Object и выполняет за вас все приведение типов.
Это означает, что дженерики Java представляют собой не что иное, как синтаксический сахар и не предлагают никакого улучшения производительности для типов значений, которые требуют упаковки / распаковки при передаче по ссылке.
Зачем использовать Generices
Вкратце, универсальные типы позволяют типам (классам и интерфейсам) быть параметрами при определении классов, интерфейсов и методов. Подобно более знакомым формальным параметрам, используемым в объявлениях методов, параметры типа предоставляют возможность повторно использовать один и тот же код с разными входными данными. Разница в том, что входными данными для формальных параметров являются значения, а входными данными для параметров типа являются типы. У оды, использующей дженерики, есть много преимуществ по сравнению с обычным кодом:
Что такое стирание шрифта
Обобщения были введены в язык Java для обеспечения более строгой проверки типов во время компиляции и для поддержки универсального программирования. Для реализации универсальных шаблонов компилятор Java применяет стирание типов к:
[NB] -Что такое мостовой метод? Вкратце, в случае параметризованного интерфейса, такого как Comparable<T>
, это может вызвать добавление дополнительных методов компилятором; эти дополнительные методы называются мостами.
Как работает стирание
Стирание типа определяется следующим образом: удалить все параметры типа из параметризованных типов и заменить любую переменную типа стиранием ее границы или объектом, если у него нет границы, или стиранием самой левой границы, если она имеет множественные границы. Вот некоторые примеры:
List<Integer>
, List<String>
и List<List<String>>
является List
.List<Integer>[]
есть List[]
.List
себя аналогично для любого необработанного типа.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 понравится :)
Есть хорошие объяснения. Я лишь добавляю пример, чтобы показать, как стирание типа работает с декомпилятором.
Оригинальный класс,
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);
}
}