В чем заключается концепция стирания в дженериках в Java?

В чем заключается концепция стирания в дженериках в Java?

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
148
0
43 342
8
Перейти к ответу Данный вопрос помечен как решенный

Ответы 8

Ответ принят как подходящий

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

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

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

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

Этот ответ фактически неверен: например, если у меня есть класс с двумя полями типов List <String> и List <Date>, то во время выполнения они будут иметь разные общие типы. Подробнее см. java.sun.com/javase/6/docs/api/java/lang/reflect/….

Rogério 28.12.2009 03:05

@Rogerio: Нет, у объекты не будет разных общих типов. поля знает типы, а объекты - нет.

Jon Skeet 28.12.2009 11:55

Да, но вы не совсем это указали. Выражение вроде «Во время выполнения List <String> и List <Date> абсолютно одинаковы» можно легко интерпретировать как более общее, чем оно есть на самом деле. Почему вы никогда не упоминали, что много информации о типах действительно доступно во время выполнения? Мне кажется, я настроен против Java ...

Rogério 29.12.2009 05:29

@Rogerio: Мне это утверждение кажется довольно недвусмысленным. List<String> - это объект, и его является то же самое, что и List<Date>. Если бы я имел в виду List<String>поле, я бы так и сказал. Я интенсивно программирую как на Java, так и на C#, и набираю стирание является, что очень сложно. Похоже, вы пытаетесь намекнуть, что это никогда не проблема.

Jon Skeet 29.12.2009 10:24

Конечно, в определенных ситуациях это проблема. Однако вы, кажется, подразумеваете, что это всегда или почти всегда проблема; мой опыт говорит об обратном. Кстати, можете ли вы привести конкретный и реалистичный пример проблемы, связанной с обобщениями, которую можно решить во время выполнения на C#, но не на Java? (Я уверен, что такие проблемы существуют, но признаю, что не знаком с ними ...)

Rogério 29.12.2009 16:03

@Rogerio: Абсолютно точно - во время выполнения очень легко узнать, является ли, например, то, что предоставляется только как Object (в слабо типизированном сценарии), на самом деле List<String>). В Java это просто невозможно - вы можете узнать, что это ArrayList, но не то, что было исходным универсальным типом. Такие вещи могут возникнуть, например, в ситуациях сериализации / десериализации. Другой пример: контейнер должен иметь возможность создавать экземпляры своего универсального типа - вы должны передавать этот тип отдельно в Java (как Class<T>).

Jon Skeet 29.12.2009 16:15

Я никогда не утверждал, что это всегда или почти всегда было проблемой, но, по моему опыту, это как минимум разумно. Есть различные места, где я вынужден добавить параметр Class<T> в конструктор (или общий метод) просто потому, что Java не сохраняет эту информацию. Взгляните, например, на EnumSet.allOf - аргумента универсального типа для метода должно быть достаточно; зачем мне указывать еще и «нормальный» аргумент? Ответ: типа стирание. Подобные вещи загрязняют API. Ради интереса, часто ли вы использовали .NET-дженерики? (продолжение)

Jon Skeet 29.12.2009 16:18

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

Jon Skeet 29.12.2009 16:21

Правда, фактическая буква «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 содержит улучшения для дженериков.

Rogério 30.12.2009 03:18

Моя основная мысль во всем этом обсуждении заключается в том, что существует МНОГОЕ, что разработчик Java может сделать с общей информацией о типах во время выполнения через Reflection API. В качестве «реального» примера см. Этот ответ, который я опубликовал некоторое время назад: stackoverflow.com/questions/1170708/…. Итак, обобщенные Java-шаблоны могут иметь ограничения, но это далеко не просто «синтаксический сахар», как думают некоторые (не вы, Джон).

Rogério 30.12.2009 03:49

@Rogerio: может много чего делает с отражением, но я не склонен обнаруживать, что хочу делает эти вещи почти так же часто, как то, что я не могу делаю с дженериками Java. Я не хочу выяснять аргумент типа для поля Около так часто, как я хочу узнать аргумент типа фактического объекта.

Jon Skeet 30.12.2009 10:54

Я не знаю ... «узнать аргумент типа реального объекта» звучит так, как будто вы предпочитаете условную логику полиморфизму. Разве это не было бы похоже на использование оператора instanceof, известной плохой практики (чтобы не сказать, что ему нет места)?

Rogério 30.12.2009 21:32

@Rogerio: Если злоупотреблять, это явно будет плохо - но, как вы говорите, у этих вещей есть свое место ... а это значит, что им полезно быть возможный. Однако я на самом деле не говорил, что использую условную логику - такие вещи, как создание нового экземпляра аргумента типа, не являются условными, но они полагаются на аргумент типа, известный во время выполнения.

Jon Skeet 30.12.2009 21:39

Я понимаю, что стирание типа в Java означает: List <E> станет «List of Object» во время выполнения. Тем не менее, объекты внутри этого списка по-прежнему сохраняют свой тип, поэтому почему Java не поддерживает instance of E. благодаря. например, мы не используем общий, но мы используем необработанный список, затем мы вставляем много объектов (любых типов). После этого мы можем получить каждый объект и использовать оператор instance_of. почему это не должно работать для дженерика.

Trần Kim Dự 12.02.2017 10:29

«Тем не менее, объекты внутри этого списка по-прежнему сохраняют свой тип, так почему же Java не позволяет использовать экземпляр E.» Поскольку он не знает, что такое E во время выполнения ... как он может проверить, является ли объект экземпляром неизвестного ему типа?

Jon Skeet 12.02.2017 10:41

@JonSkeet «Скомпилированный универсальный код на самом деле просто использует java.lang.Object» Я думаю, что он действительно использует нижнюю границу универсального типа. Например. для <T extends List> будет использоваться java.util.List.

Stijn de Witt 23.03.2017 00:05

Насколько я понимаю (будучи парнем из .СЕТЬ), JVM не имеет понятия дженериков, поэтому компилятор заменяет параметры типа на Object и выполняет все приведение типов за вас.

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

Дженерики Java в любом случае не могут представлять типы значений - не существует такой вещи, как List <int>. Однако в Java нет передачи по ссылке вообще - она ​​проходит строго по значению (где это значение может быть ссылкой).

Jon Skeet 24.11.2008 10:31

Чтобы завершить уже очень полный ответ Джона Скита, вы должны осознать, что концепция стирание типа проистекает из необходимости совместимость с предыдущими версиями 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.Iterator class 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. Другими словами, именно этот третий пункт является наиболее важным. (Продолжение.)

Jon Skeet 24.11.2008 10:55

Лично я считаю, что это была близорукая ошибка - она ​​давала краткосрочное преимущество и давала долгосрочный недостаток.

Jon Skeet 24.11.2008 10:55

Дополняя уже дополненный ответ Джона Скита ...

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

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

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

Неудобство не в том, что вы хотите делать «непослушные» вещи во время выполнения. Это когда вы хотите делать совершенно разумные вещи во время выполнения. Фактически, стирание типа позволяет вам делать гораздо более непослушные вещи - например, преобразовывать List <String> в List, а затем в List <Date> только с предупреждениями.

Jon Skeet 24.11.2008 12:13

В качестве примечания: это интересное упражнение, чтобы на самом деле увидеть, что делает компилятор, когда он выполняет стирание - упрощает понимание всей концепции. Есть специальный флаг, который вы можете передать компилятору для вывода 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 сказал, работает.

frank 24.06.2018 04:14

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

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

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, если параметры типа не ограничены. Таким образом, полученный байт-код содержит только обычные классы, интерфейсы и методы.
  • При необходимости вставьте отливки типа, чтобы сохранить безопасность типа.
  • Сгенерируйте методы моста для сохранения полиморфизма в расширенных универсальных типах.

[N.B.] - Что такое мостовой метод? Короче говоря, в случае параметризованного интерфейса, такого как 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)

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

Надеюсь, Читателю понравится :)

Другие вопросы по теме