



Это в основном способ реализации дженериков в Java с помощью уловки компилятора. Скомпилированный общий код на самом деле просто использует java.lang.Object везде, где вы говорите о T (или каком-либо другом параметре типа) - и есть некоторые метаданные, чтобы сообщить компилятору, что это действительно общий тип.
Когда вы компилируете некоторый код для общего типа или метода, компилятор определяет, что вы на самом деле имеете в виду (то есть, что такое аргумент типа для T), и проверяет во время компилировать, что вы делаете правильные вещи, но полученный код снова просто говорит в терминах java.lang.Object - компилятор генерирует дополнительные преобразования там, где это необходимо. Во время выполнения List<String> и List<Date> абсолютно одинаковы; дополнительная информация о типе была стертый компилятором.
Сравните это, скажем, с C#, где информация сохраняется во время выполнения, позволяя коду содержать такие выражения, как typeof(T), который эквивалентен T.class, за исключением того, что последний недействителен. (Имейте в виду, что между универсальными шаблонами .NET и универсальными шаблонами Java существуют и другие различия.) Стирание типов является источником многих «странных» предупреждений / сообщений об ошибках при работе с универсальными шаблонами Java.
Другие источники:
@Rogerio: Нет, у объекты не будет разных общих типов. поля знает типы, а объекты - нет.
Да, но вы не совсем это указали. Выражение вроде «Во время выполнения List <String> и List <Date> абсолютно одинаковы» можно легко интерпретировать как более общее, чем оно есть на самом деле. Почему вы никогда не упоминали, что много информации о типах действительно доступно во время выполнения? Мне кажется, я настроен против Java ...
@Rogerio: Мне это утверждение кажется довольно недвусмысленным. List<String> - это объект, и его является то же самое, что и List<Date>. Если бы я имел в виду List<String>поле, я бы так и сказал. Я интенсивно программирую как на Java, так и на C#, и набираю стирание является, что очень сложно. Похоже, вы пытаетесь намекнуть, что это никогда не проблема.
Конечно, в определенных ситуациях это проблема. Однако вы, кажется, подразумеваете, что это всегда или почти всегда проблема; мой опыт говорит об обратном. Кстати, можете ли вы привести конкретный и реалистичный пример проблемы, связанной с обобщениями, которую можно решить во время выполнения на C#, но не на Java? (Я уверен, что такие проблемы существуют, но признаю, что не знаком с ними ...)
@Rogerio: Абсолютно точно - во время выполнения очень легко узнать, является ли, например, то, что предоставляется только как Object (в слабо типизированном сценарии), на самом деле List<String>). В Java это просто невозможно - вы можете узнать, что это ArrayList, но не то, что было исходным универсальным типом. Такие вещи могут возникнуть, например, в ситуациях сериализации / десериализации. Другой пример: контейнер должен иметь возможность создавать экземпляры своего универсального типа - вы должны передавать этот тип отдельно в Java (как Class<T>).
Я никогда не утверждал, что это всегда или почти всегда было проблемой, но, по моему опыту, это как минимум разумно. Есть различные места, где я вынужден добавить параметр Class<T> в конструктор (или общий метод) просто потому, что Java не сохраняет эту информацию. Взгляните, например, на EnumSet.allOf - аргумента универсального типа для метода должно быть достаточно; зачем мне указывать еще и «нормальный» аргумент? Ответ: типа стирание. Подобные вещи загрязняют API. Ради интереса, часто ли вы использовали .NET-дженерики? (продолжение)
До того, как я начал использовать дженерики .NET, я обнаружил, что дженерики Java по-разному неудобны (и использование подстановочных знаков по-прежнему является головной болью, хотя форма дисперсии, определяемая вызывающим абонентом, определенно имеет свои преимущества) - но это произошло только после того, как я использовал дженерики .NET какое-то время я видел, сколько шаблонов стало неудобным или невозможным с дженериками Java. Опять парадокс Блаба. Я не говорю, что у дженериков .NET тоже нет недостатков, кстати - существуют различные отношения типов, которые, к сожалению, не могут быть выражены, - но я предпочитаю их дженерикам Java.
Правда, фактическая буква «E» в List<E> не может быть обнаружена во время выполнения только из экземпляра. Однако сериализация Java обеспечивает полный контроль над процессом (методы readObject / writeObject). Но да, я вижу, что EnumSet<MyEnum>.allOf() лучше, чем EnumSet.allOf(MyEnum.class). Я использовал только .NET 1.1 еще в 2002 году (до того, как в C# 2.0 были добавлены дженерики). Дженерики Java, несомненно, временами неудобны. Возможно, он будет еще улучшен в Java 7 или 8 (если они действительно будут выпущены); в конце концов, даже C# 4.0 содержит улучшения для дженериков.
Моя основная мысль во всем этом обсуждении заключается в том, что существует МНОГОЕ, что разработчик Java может сделать с общей информацией о типах во время выполнения через Reflection API. В качестве «реального» примера см. Этот ответ, который я опубликовал некоторое время назад: stackoverflow.com/questions/1170708/…. Итак, обобщенные Java-шаблоны могут иметь ограничения, но это далеко не просто «синтаксический сахар», как думают некоторые (не вы, Джон).
@Rogerio: может много чего делает с отражением, но я не склонен обнаруживать, что хочу делает эти вещи почти так же часто, как то, что я не могу делаю с дженериками Java. Я не хочу выяснять аргумент типа для поля Около так часто, как я хочу узнать аргумент типа фактического объекта.
Я не знаю ... «узнать аргумент типа реального объекта» звучит так, как будто вы предпочитаете условную логику полиморфизму. Разве это не было бы похоже на использование оператора instanceof, известной плохой практики (чтобы не сказать, что ему нет места)?
@Rogerio: Если злоупотреблять, это явно будет плохо - но, как вы говорите, у этих вещей есть свое место ... а это значит, что им полезно быть возможный. Однако я на самом деле не говорил, что использую условную логику - такие вещи, как создание нового экземпляра аргумента типа, не являются условными, но они полагаются на аргумент типа, известный во время выполнения.
Я понимаю, что стирание типа в Java означает: List <E> станет «List of Object» во время выполнения. Тем не менее, объекты внутри этого списка по-прежнему сохраняют свой тип, поэтому почему Java не поддерживает instance of E. благодаря. например, мы не используем общий, но мы используем необработанный список, затем мы вставляем много объектов (любых типов). После этого мы можем получить каждый объект и использовать оператор instance_of. почему это не должно работать для дженерика.
«Тем не менее, объекты внутри этого списка по-прежнему сохраняют свой тип, так почему же Java не позволяет использовать экземпляр E.» Поскольку он не знает, что такое E во время выполнения ... как он может проверить, является ли объект экземпляром неизвестного ему типа?
@JonSkeet «Скомпилированный универсальный код на самом деле просто использует java.lang.Object» Я думаю, что он действительно использует нижнюю границу универсального типа. Например. для <T extends List> будет использоваться java.util.List.
Насколько я понимаю (будучи парнем из .СЕТЬ), JVM не имеет понятия дженериков, поэтому компилятор заменяет параметры типа на Object и выполняет все приведение типов за вас.
Это означает, что дженерики Java представляют собой не что иное, как синтаксический сахар и не предлагают никакого улучшения производительности для типов значений, которые требуют упаковки / распаковки при передаче по ссылке.
Дженерики Java в любом случае не могут представлять типы значений - не существует такой вещи, как List <int>. Однако в Java нет передачи по ссылке вообще - она проходит строго по значению (где это значение может быть ссылкой).
Чтобы завершить уже очень полный ответ Джона Скита, вы должны осознать, что концепция стирание типа проистекает из необходимости совместимость с предыдущими версиями Java.
Первоначально представленный на EclipseCon 2007 (больше не доступен), совместимость включала следующие моменты:
Оригинальный ответ:
Следовательно:
new ArrayList<String>() => new ArrayList()
Есть предложения по увеличению овеществление. Reify быть «Считайте абстрактное понятие реальным», где языковые конструкции должны быть концепциями, а не просто синтаксическим сахаром.
Я также должен упомянуть метод checkCollection Java 6, который возвращает динамически безопасное представление указанной коллекции. Любая попытка вставить элемент неправильного типа немедленно приведет к ClassCastException.
Механизм дженериков на языке обеспечивает (статическую) проверку типов во время компиляции, но этот механизм можно обойти с помощью непроверенных приведений.
Обычно это не проблема, поскольку компилятор выдает предупреждения обо всех таких непроверенных операциях.
Однако бывают случаи, когда одной только статической проверки типов недостаточно, например:
ClassCastException, указывая на то, что неправильно типизированный элемент был помещен в параметризованную коллекцию. К сожалению, исключение может произойти в любое время после вставки ошибочного элемента, поэтому обычно оно предоставляет мало или совсем не дает информации о реальном источнике проблемы.Обновление от июля 2012 года, почти четыре года спустя:
Сейчас (2012) подробно описано в "Правила совместимости миграции API (тест подписи)"
The Java programming language implements generics using erasure, which ensures that legacy and generic versions usually generate identical class files, except for some auxiliary information about types. Binary compatibility is not broken because it is possible to replace a legacy class file with a generic class file without changing or recompiling any client code.
To facilitate interfacing with non-generic legacy code, it is also possible to use the erasure of a parameterized type as a type. Such a type is called a raw type (Java Language Specification 3/4.8). Allowing the raw type also ensures backward compatibility for source code.
According to this, the following versions of the
java.util.Iteratorclass are both binary and source code backward compatible:
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();
}
Обратите внимание, что обратная совместимость могла быть достигнута без стирания типов, но не без того, чтобы программисты Java изучили новый набор коллекций. Это именно тот путь, по которому пошел .NET. Другими словами, именно этот третий пункт является наиболее важным. (Продолжение.)
Лично я считаю, что это была близорукая ошибка - она давала краткосрочное преимущество и давала долгосрочный недостаток.
Дополняя уже дополненный ответ Джона Скита ...
Было упомянуто, что реализация дженериков посредством стирания приводит к некоторым досадным ограничениям (например, отсутствие new T[42]). Также было упомянуто, что основной причиной этого была обратная совместимость в байт-коде. Это тоже (в основном) правда. Сгенерированный байт-код -target 1.5 несколько отличается от просто лишенного сахара приведения -target 1.4. Технически даже возможно (с помощью огромных уловок) получить доступ к экземплярам универсального типа во время выполнения, доказывая, что в байт-коде действительно что-то есть.
Более интересным моментом (который не поднимался) является то, что реализация универсальных шаблонов с использованием стирания предлагает немного больше гибкости в том, что может выполнять система типов высокого уровня. Хорошим примером этого может быть реализация Scala JVM по сравнению с CLR. В JVM можно напрямую реализовать высшие типы, поскольку сама JVM не накладывает ограничений на универсальные типы (поскольку эти «типы» фактически отсутствуют). Это контрастирует с CLR, которая знает экземпляры параметров во время выполнения. Из-за этого сама среда CLR должна иметь некоторое представление о том, как следует использовать универсальные шаблоны, сводя на нет попытки расширить систему с помощью непредвиденных правил. В результате высшие типы Scala в CLR реализуются с использованием странной формы стирания, эмулируемой в самом компиляторе, что делает их не полностью совместимыми с простыми старыми универсальными шаблонами .NET.
Стирание может быть неудобным, когда вы хотите делать непослушные вещи во время выполнения, но оно предлагает максимальную гибкость для авторов компилятора. Я предполагаю, что это одна из причин, почему он не исчезнет в ближайшее время.
Неудобство не в том, что вы хотите делать «непослушные» вещи во время выполнения. Это когда вы хотите делать совершенно разумные вещи во время выполнения. Фактически, стирание типа позволяет вам делать гораздо более непослушные вещи - например, преобразовывать List <String> в List, а затем в List <Date> только с предупреждениями.
В качестве примечания: это интересное упражнение, чтобы на самом деле увидеть, что делает компилятор, когда он выполняет стирание - упрощает понимание всей концепции. Есть специальный флаг, который вы можете передать компилятору для вывода 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();
}
}
Я попытался использовать декомпилятор java, чтобы увидеть код после стирания типа из файла .class, но в файле .class все еще есть информация о типе. Пробовал jigawot сказал, работает.
Есть хорошие объяснения. Я лишь добавляю пример, чтобы показать, как стирание типа работает с декомпилятором.
Оригинальный класс,
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 применяет стирание типов к:
[N.B.] - Что такое мостовой метод? Короче говоря, в случае параметризованного интерфейса, такого как 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)
Следовательно, во время компиляции сообщается о конфликте имен. Невозможно дать оба методы с тем же именем и попытайтесь различить их путем перегрузки, потому что после стирания невозможно отличить вызов одного метода от другого.
Надеюсь, Читателю понравится :)
Этот ответ фактически неверен: например, если у меня есть класс с двумя полями типов List <String> и List <Date>, то во время выполнения они будут иметь разные общие типы. Подробнее см. java.sun.com/javase/6/docs/api/java/lang/reflect/….