Если поле типизировано дженериком как в байт коде будет представлен этот тип java
Перейти к содержимому

Если поле типизировано дженериком как в байт коде будет представлен этот тип java

  • автор:

Стирание типов

Стирание типов - 1

Привет! Мы продолжаем серию лекций о дженериках. Ранее мы разобрались в общих чертах, что это такое, и зачем нужно. Сегодня поговорим о некоторых особенностях дженериков и рассмотрим некоторые подводные камни при работе с ними. Поехали! В прошлой лекции мы говорили о разнице между Generic Types и Raw Types . Если ты забыл, Raw Type — это класс-дженерик, из которого удалили его тип. Вот пример. Здесь мы не указываем, какого именно типа объекты будут помещаться в наш List . Если попытаться создать такой List и добавить в него какие-то объекты мы увидим в IDEa предупреждение: Но также мы говорили о том, что дженерики появились только в версии языка Java 5. К моменту ее выхода программисты успели написать кучу кода с использованием Raw Types, и чтобы он не перестал работать, возможность создания и работы Raw Types в Java сохранилась. Однако эта проблема оказалась гораздо обширнее. Java-код, как ты знаешь, преобразуется в специальный байт-код, который потом выполняется виртуальной машиной Java. И если бы в процессе перевода мы помещали в байт-код информацию о типах-параметрах, это сломало бы весь ранее написанный код, ведь до Java 5 никаких типов-параметров не существовало! При работе с дженериками есть одна очень важная особенность, о которой необходимо помнить. Она называется “стирание типов” (type erasure). Ее суть заключается в том, что внутри класса не хранится никакой информации о его типе-параметре. Эта информация доступна только на этапе компиляции и стирается (становится недоступной) в runtime. Если ты попытаешься положить объект не того типа в свой List<String> , компилятор выдаст ошибку. Этого как раз и добивались создатели языка, создавая дженерики — проверки на этапе компиляции. Но когда весь написанный тобой Java-код превратится в байт-код, в нем не будет информации о типах-параметрах. Внутри байт-кода твой список List<Cat> cats не будет отличаться от List<String> strings. В байт-коде ничто не будет говорить о том, что cats — это список объектов Cat . Информация об этом сотрется во время компиляции, и в байт код попадет только информация о том, что у тебя в программе есть некий список List<Object> cats . Давай посмотрим как это работает: Мы создали собственный дженерик-класс TestClass . Он довольно прост: по сути это небольшая “коллекция” на 2 объекта, которые помещаются туда сразу же при создании объекта. В качестве полей у него 2 объекта T . При выполнении метода createAndAdd2Values() должно произойти приведение двух переданных объектов Object a и Object b к нашему типу T , после чего они будут добавлены в объект TestClass . В методе main() мы создаем TestClass<Integer> , то есть в качестве T у нас будет Integer . Но при этом в метод createAndAdd2Values() мы передаем число Double и объект String . Как ты думаешь, сработает ли наша программа? Ведь в качестве типа-параметра мы указали Integer , а String точно нельзя привести к Integer ! Давай запустим метод main() и проверим. Вывод в консоль: 22.111 Test String Неожиданный результат! Почему такое произошло? Именно из-за стирания типов. Во время компиляции кода информация о типе-параметре Integer нашего объекта TestClass<Integer> test стерлась. Он превратился в TestClass<Object> test . Наши параметры Double и String без проблем преобразовались в Object (а не в Integer , как мы того ожидали!) и спокойно добавились в TestClass . Вот еще один простой, но очень показательный пример стирания типов: Вывод в консоль: true true Казалось бы, мы создали коллекции с тремя разными типами-параметрами — String , Integer , и созданный нами класс Cat . Но во время преобразования в байт-код все три списка превратились в List<Object> , поэтому при выполнении программа говорит нам, что во всех трех случаях у нас используется один и тот же класс.

Стирание типов при работе с массивами и дженериками

Стирание типов - 2

Есть один очень важный момент, который необходимо четко понимать при работе с массивами и дженериками (например, List ). Также его стоит учитывать при выборе структуры данных для твоей программы. Дженерики подвержены стиранию типов. Информация о типе-параметре недоступна во время выполнения программы. В отличие от них, массивы знают и могут использовать информацию о своем типе данных во время выполнения программы. Попытка поместить в массив значение неверного типа приведет к исключению: Вывод в консоль: Из-за того, что между массивами и дженериками есть такая большая разница, у них могут возникнуть проблемы с совместимостью. Прежде всего, ты не можешь создать массив объектов-дженериков или даже просто типизированный массив. Звучит немного непонятно? Давай рассмотрим наглядно. К примеру, ты не сможешь сделать в Java ничего из этого: Если мы попытаемся создать массив списков List<String> , получим ошибку компиляции generic array creation: Но для чего это сделано? Почему создание таких массивов запрещено? Это все — для обеспечения типобезопасности. Если бы компилятор позволял нам создавать такие массивы из объектов-дженериков, мы могли бы заработать кучу проблем. Вот простой пример из книги Джошуа Блоха “Effective Java”: Давай представим, что создание массива List<String>[] stringLists было бы разрешено, и компилятор бы не ругался. Вот каких дел мы могли бы наворотить в этом случае: В строке 1 мы создаем массив листов List<String>[] stringLists . Наш массив вмещает в себя один List<String> . В строке 2 мы создаем список чисел List<Integer> . В строке 3 мы присваиваем наш массив List<String>[] в переменную Object[] objects . Язык Java позволяет это делать: в массив объектов X можно помещать и объекты X , и объекты всех дочерних классов Х . Соответственно, в массив Objects можно поместить вообще все что угодно. В строке 4 мы подменяем единственный элемент массива objects (List<String>) на список List<Integer> . В результате мы поместили List<Integer> в наш массив, который предназначался только для хранения List<String> ! С ошибкой же мы столкнемся только когда код дойдет до строки 5. Во время выполнения программы будет выброшено исключение ClassCastException . Поэтому запрет на создание таких массивов и был введен в язык Java — это позволяет нам избегать подобных ситуаций.

Как можно обойти стирание типов?

Стирание типов - 3

Что ж, стирание типов мы изучили. Давай попробуем обмануть систему! 🙂 Задача: У нас есть класс-дженерик TestClass<T> . Нам нужно создать в нем метод createNewT() , который будет создавать и возвращать новый объект типа Т . Но ведь это невозможно сделать, так? Вся информация о типе Т будет стерта во время компиляции, и в процессе работы программы мы не сможем узнать, объект какого именно типа нам нужно создать. На самом деле, есть один хитрый способ. Ты наверняка помнишь, что в Java есть класс Class . Используя его, мы можем получить класс любого нашего объекта: Вывод в консоль: Но вот одна особенность, о которой мы не говорили. В документации Oracle ты увидишь, что класс Class — это дженерик! В документации написано: “Т — это тип класса, моделируемого этим объектом Class”. Если перевести это с языка документации на человеческий, это означает, что классом для объекта Integer.class является не просто Class , а Class<Integer> . Типом объекта string.class является не просто Class , Class<String> , и т.д. Если все еще непонятно, попробуй добавить тип-параметр к предыдущему примеру: И вот теперь, используя это знание, мы можем обойти стирание типов и решить нашу задачу! Попробуем получить информацию о типе-параметре. Его роль будет играть класс MySecretClass : А вот как мы используем на практике наше решение: Вывод в консоль: Мы просто передали нужный класс-параметр в конструктор нашего класса-дженерика: Благодаря этому мы сохранили информацию о типе-параметре и уберегли ее от стирания. В итоге мы смогли создать объект T ! 🙂 На этом сегодняшняя лекция подходит к концу. О стирании типов всегда необходимо помнить при работе с дженериками. Выглядит это дело не очень удобно, но нужно понимать — дженерики не были частью языка Java при его создании. Это позже прикрученная возможность, которая помогает нам создавать типизированные коллекции и отлавливать ошибки на этапе компиляции. В некоторых других языках, где дженерики появлялись с первой версии, стирание типов отсутствует (например, в C#). Впрочем, мы не закончили изучение дженериков! На следующей лекции ты познакомишься с еще несколькими особенностями работы с ними. А пока было бы неплохо решить пару задач! 🙂

Java Language
Дженерики

Ezoic

report this ad

Generics — это средство общего программирования, которое расширяет систему типов Java, чтобы позволить типу или методу работать на объектах разных типов, обеспечивая при этом безопасность типа времени компиляции. В частности, среда коллекций Java поддерживает дженерики, чтобы указать тип объектов, хранящихся в экземпляре коллекции.

Синтаксис

  • class ArrayList <E> <> // общий класс с параметром типа E
  • класс HashMap <K, V> <> // общий класс с двумя параметрами типа K и V
  • <E> void print (элемент E) <> // общий метод с параметром типа E
  • ArrayList <String> имена; // объявление общего класса
  • ArrayList <?> Объекты; // объявление общего класса с неизвестным параметром типа
  • new ArrayList <String> () // создание экземпляра универсального класса
  • new ArrayList <> () // экземпляр с типом вывода "diamond" (Java 7 или новее)

замечания

Генерики реализуются в Java через стирание типа, что означает, что во время выполнения информация типа, указанная в создании экземпляра универсального класса, недоступна. Например, оператор List<String> names = new ArrayList<>(); создает объект списка, из которого тип элемента String не может быть восстановлен во время выполнения. Однако, если список хранится в поле типа List<String> или передается параметру method / constructor этого же типа или возвращается из метода этого типа возврата, то полная информация о типе может быть восстановлена ​​во время выполнения через API Java Reflection.

Это также означает, что при выдаче родовому типу (например: (List<String>) list ), литой является непроверенный бросок . Поскольку параметр <String> стирается, JVM не может проверить правильность приведения из List<?> List<String> ; JVM видит только показ List для List во время выполнения.

Создание общего класса

Generics позволяет классам, интерфейсам и методам брать другие классы и интерфейсы в качестве параметров типа.

В этом примере используется общий класс Param для принятия одного параметра типа T , ограниченного угловыми скобками ( <> ):

Чтобы создать экземпляр этого класса, укажите аргумент типа вместо T Например, Integer :

Аргумент типа может быть любым ссылочным типом, включая массивы и другие общие типы:

В Java SE 7 и более поздних версиях аргумент типа может быть заменен пустым набором аргументов типа ( <> ), называемым алмазом :

В отличие от других идентификаторов, параметры типа не имеют ограничений именования. Однако их имена обычно являются первой буквой их цели в верхнем регистре. (Это верно даже во всех официальных JavaDocs.)
Примеры включают T для «type» , E для «element» и K / V для «key» / «value» .

Расширение общего класса

AbstractParam — абстрактный класс, объявленный с параметром типа T При расширении этого класса этот параметр типа может быть заменен аргументом типа, записанным внутри <> , или параметр типа может оставаться неизменным. В первом и втором примерах ниже String и Integer заменяют параметр типа. В третьем примере параметр типа остается неизменным. В четвертом примере вообще не используются обобщения, поэтому он аналогичен классу Object . Компилятор будет предупреждать о том, что AbstractParam является сырым типом, но он скомпилирует класс ObjectParam . Пятый пример имеет 2 типа параметров (см. «Параметры нескольких типов» ниже), выбирая второй параметр как параметр типа, переданный суперклассу.

Ниже приведено использование:

Обратите внимание, что в классе Email метод T getValue() действует так, как будто он имеет подпись String getValue() , а метод void setValue(T) действует так, как если бы он был объявлен void setValue(String) .

Также возможно создать экземпляр с анонимным внутренним классом с пустыми фигурными фигурными скобками ( <> ):

Множественные параметры типа

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

Использование может быть выполнено следующим образом:

Объявление общего метода

Методы также могут иметь общие параметры типа.

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

Иногда, хотя и редко, может быть необходимо переопределить этот тип вывода с явными аргументами типа:

Это необходимо в этом примере, потому что компилятор не может «смотреть вперед», чтобы увидеть, что Object нужен для T после вызова stream() и в противном случае он makeList бы String на makeList аргументов makeList . Обратите внимание, что язык Java не поддерживает исключение класса или объекта, на который вызывается метод ( this в приведенном выше примере), когда аргументы типа явно указаны.

Бриллиант

Java 7 представила Diamond 1, чтобы удалить некоторую котельную плиту вокруг создания экземпляра универсального класса. С Java 7+ вы можете написать:

Там, где вам приходилось писать в предыдущих версиях, это:

Одно ограничение — для анонимных классов , где вы все равно должны указывать параметр типа в экземпляре:

Хотя использование алмаза с анонимными внутренними классами не поддерживается в Java 7 и 8, оно будет включено как новая функция в Java 9 .

1 — Некоторые люди называют <> использование «алмазного оператора ». Это неверно. Алмаз не ведет себя как оператор и не описан или не указан нигде в JLS или (официальном) Java-учебном пособии как оператор. Действительно, <> не является даже явным символом Java. Скорее это < токен, за которым следует токен > , и это законный (хотя и плохой стиль), чтобы иметь пробелы или комментарии между ними. JLS и Tutorials последовательно ссылаются на <> как на «алмаз», и поэтому это правильный термин для него.

Требование нескольких верхних границ («расширяет A & B»)

Вы можете потребовать, чтобы общий тип расширил несколько верхних границ.

Пример: мы хотим отсортировать список чисел, но Number не реализует Comparable .

В этом примере T должен расширять Number и реализовывать Comparable<T> который должен соответствовать всем «нормальным» реализациям встроенных чисел, таких как Integer или BigDecimal но не подходит для более экзотических, таких как Striped64 .

Поскольку множественное наследование не разрешено, вы можете использовать не более одного класса в качестве привязки и должны быть первыми перечислены. Например, <T extends Comparable<T> & Number> не допускается, потому что Comparable является интерфейсом, а не классом.

Создание ограниченного общего класса

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

Без ограниченных дженериков мы не можем создать класс-контейнер, который является общим и знает, что каждый элемент является животным:

С общим ограничением в определении класса это теперь возможно.

Это также ограничивает действительные экземпляры родового типа:

Решаем между `T`,`? супер T` и `? расширяет T`

Синтаксис Java генерирует ограниченные подстановочные знаки, представляя неизвестный тип ? является:

? extends T представляет собой верхний ограниченный шаблон. Неизвестный тип представляет тип, который должен быть подтипом T или сам тип T.

? super T представляет собой нижний ограниченный шаблон. Неизвестный тип представляет тип, который должен быть супертипом T или самим типом T.

Как правило, вы должны использовать

  • ? extends T если вам нужен только «чтение» доступа («ввод»)
  • ? super T если вам нужно «написать» доступ («выход»)
  • T если вам нужны оба («изменить»)

Использование extends или super обычно лучше, потому что это делает ваш код более гибким (как в: разрешении использования подтипов и супертипов), как вы увидите ниже.

Теперь компилятор сможет обнаружить определенное плохое использование:

Выбрав правильный T ? super T или ? extends T необходимо для использования с подтипами. Затем компилятор может обеспечить безопасность типа; вам не нужно бросать (который не является безопасным по типу и может вызвать ошибки программирования), если вы используете их правильно.

Если это непросто понять, пожалуйста, помните правило PECS :

P roducer использует « E xtends», а C onsumer использует « S uper».

(У производителя есть только доступ на запись, и у Потребителя есть только доступ для чтения)

Преимущества универсального класса и интерфейса

Код, который использует generics, имеет много преимуществ по сравнению с нестандартным кодом. Ниже приведены основные преимущества

Более строгие проверки во время компиляции

Компилятор Java применяет сильную проверку типов к универсальному коду и выдает ошибки, если код нарушает безопасность типов. Исправить ошибки времени компиляции проще, чем исправлять ошибки времени выполнения, которые могут быть трудно найти.

Устранение отливок

Следующий фрагмент кода без дженериков требует кастинга:

При повторном написании для использования дженериков код не требует кастинга:

Включение программистов для реализации общих алгоритмов

Используя generics, программисты могут реализовывать общие алгоритмы, которые работают с коллекциями разных типов, могут быть настроены и безопасны в типе и их легче читать.

Связывание общего параметра с более чем 1 типом

Общие параметры также могут быть привязаны к нескольким типам с использованием синтаксиса T extends Type1 & Type2 & .

Предположим, вы хотите создать класс, универсальный тип которого должен реализовывать как Flushable и Closeable , вы можете написать

Теперь ExampleClass принимает только общие параметры, типы, которые реализуют как Flushable и Closeable .

Методы класса могут выбирать вывод общих аргументов типа как Closeable и Flushable .

Замечания:

Вы не можете привязать общий параметр к любому типу, используя предложение OR ( | ). Поддерживается только предложение AND ( & ). Общий тип может расширять только один класс и многие интерфейсы. Класс должен быть помещен в начало списка.

Создание экземпляра родового типа

Из-за стирания типа следующее не будет работать:

Тип T стирается. Поскольку во время выполнения JVM не знает, что изначально было T , он не знает, какой конструктор должен вызывать.

обходные

Передача класса T при вызове genericMethod :

Который выдает исключения, поскольку нет способа узнать, имеет ли переданный класс доступный конструктор по умолчанию.

Передача ссылки на конструктор T :

Обращаясь к объявленному типовому типу в пределах его собственного объявления

Как вы собираетесь использовать экземпляр (возможно, далее) унаследованного типичного типа в объявлении метода в объявлении общего типа? Это одна из проблем, с которыми вам придется столкнуться, когда вы копаете немного глубже в дженериках, но все же довольно распространенный.

Предположим, что у нас есть DataSeries<T> (здесь), который определяет общий ряд данных, содержащий значения типа T Очень сложно работать с этим типом непосредственно, когда мы хотим выполнить много операций, например, с двойными значениями, поэтому мы определяем DoubleSeries extends DataSeries<Double> . Предположим, что исходный DataSeries<T> имеет метод add(values) который добавляет другую серию одинаковой длины и возвращает новую. Как мы применяем тип values и тип возврата как DoubleSeries а не DataSeries<Double> в нашем производном классе?

Проблема может быть решена путем добавления параметра универсального типа, возвращающего назад и расширяющего объявляемый тип (применяется к интерфейсу здесь, но то же самое относится к классам):

Здесь T представляет собой тип данных, который имеет ряд, например Double и DS . Унаследовал тип (или типы) теперь могут быть легко реализованы путем замены упомянутого выше параметра, соответствующего производного типа, таким образом, получая конкретное Double основанное определение формы:

В настоящий момент даже IDE будет реализовывать вышеупомянутый интерфейс с правильными типами на месте, что после того, как бит наполнения содержимого может выглядеть так:

Как вы можете видеть, метод add объявлен как DoubleSeries add(DoubleSeries values) и компилятор счастлив.

При необходимости шаблон может быть дополнительно вложен.

Использование экземпляра с Generics

Использование дженериков для определения типа в instanceof

Рассмотрим следующий общий класс Example объявленный с формальным параметром <T> :

Это всегда даст ошибку компиляции, поскольку, как только компилятор компилирует исходный код Java в байт-код Java, он применяет процесс, известный как стирание типа , который преобразует весь общий код в не общий код, что делает невозможным отличать T-типы во время выполнения. Тип, используемый с instanceof должен быть повторно подкрепляемым , что означает, что вся информация о типе должна быть доступна во время выполнения, и обычно это не относится к родовым типам.

Следующий класс представляет, что два разных класса Example , Example<String> и Example<Number> , выглядят так, что после того, как дженерики разделились стиранием типа :

Поскольку типы исчезли, JVM не знает, какой тип T

Исключение из предыдущего правила

Вы всегда можете использовать неограниченный шаблон (?) Для указания типа в instanceof следующим образом:

Это может быть полезно для оценки того, является ли экземпляр obj List или нет:

На самом деле неограниченный подстановочный знак считается воспроизводимым типом.

Использование общего экземпляра с instanceof

Другая сторона монеты заключается в том, что использование экземпляра t из T с instanceof является законным, как показано в следующем примере:

потому что после стирания типа класс будет выглядеть следующим образом:

Так как даже если стирание типа происходит в любом случае, теперь JVM может различать разные типы в памяти, даже если они используют один и тот же ссылочный тип ( Object ), как показано в следующем фрагменте:

Различные способы реализации универсального интерфейса (или расширения общего класса)

Предположим, что был указан следующий общий интерфейс:

Ниже перечислены возможные способы его реализации.

Внеродная реализация класса с определенным типом

Выберите определенный тип, чтобы заменить параметр формального типа <T> MyGenericClass и реализовать его, как MyGenericClass в следующем примере:

Этот класс относится только к String , и это означает, что использование MyGenericInterface с различными параметрами (например, Integer , Object и т. Д.) Не будет компилироваться, как показано в следующем фрагменте:

Общая реализация класса

Объявите другой общий интерфейс с формальным параметром типа <T> который реализует MyGenericInterface следующим образом:

Обратите внимание, что может использоваться другой формальный параметр типа:

Внедрение класса Raw

Объявите не-общий класс, который реализует MyGenericInteface как необработанный тип (вообще не используя общий):

Этот способ не рекомендуется, так как он не на 100% безопасен во время выполнения, потому что он смешивает сырой тип (подкласса) с дженериками (интерфейса), и это также запутывает. Современные компиляторы Java поднимут предупреждение с такой реализацией, однако код — по соображениям совместимости с более старым JVM (1.4 или более ранним) — скомпилируется.

Все перечисленные выше способы также разрешены при использовании универсального класса в качестве супертипа вместо общего интерфейса.

Использование Generics для автоматического создания

С помощью дженериков можно вернуть все, что ожидает абонент:

Метод будет скомпилирован с предупреждением. Код на самом деле более безопасен, чем выглядит, потому что время выполнения Java будет выполняться при его использовании:

Это менее безопасно, если вы используете общие типы:

Здесь приведение будет выполняться, когда возвращаемый тип представляет собой какой-либо List (т. ClassCastException Возвращаемый List<String> не приведет к ClassCastException , в конечном итоге вы получите его, когда ClassCastException элементы из списка).

Чтобы обойти эту проблему, вы можете создать API, который использует типизированные ключи:

наряду с этим методом put() :

При таком подходе вы не можете поместить неправильный тип в карту, поэтому результат всегда будет правильным (если вы случайно не создадите два ключа с тем же именем, но с разными типами).

Получить класс, который удовлетворяет общему параметру во время выполнения

Многие несвязанные общие параметры, такие как те, которые используются в статическом методе, не могут быть восстановлены во время выполнения (см. Другие темы на Erasure ). Однако существует общая стратегия, используемая для доступа к типу, удовлетворяющему параметру generic для класса во время выполнения. Это позволяет использовать общий код, который зависит от доступа к типу, без необходимости передавать информацию типа типа через каждый вызов.

Фон

Общая параметризация в классе может быть проверена путем создания анонимного внутреннего класса. Этот класс будет захватывать информацию о типе. В общем, этот механизм упоминается как токены супертекста , которые подробно описаны в блоге Neal Gafter .

Пришел, увидел, обобщил: погружаемся в Java Generics

Java Generics — это одно из самых значительных изменений за всю историю языка Java. «Дженерики», доступные с Java 5, сделали использование Java Collection Framework проще, удобнее и безопаснее. Ошибки, связанные с некорректным использованием типов, теперь обнаруживаются на этапе компиляции. Да и сам язык Java стал еще безопаснее. Несмотря на кажущуюся простоту обобщенных типов, многие разработчики сталкиваются с трудностями при их использовании. В этом посте я расскажу об особенностях работы с Java Generics, чтобы этих трудностей у вас было поменьше. Пригодится, если вы не гуру в дженериках, и поможет избежать много трудностей при погружении в тему.

Работа с коллекциями

Предположим, банку нужно подсчитать сумму сбережений на счетах клиентов. До появления «дженериков» метод вычисления суммы выглядел так:

Мы итерировались, пробегались по списку аккаунтов и проверяли, действительно ли элемент из этого списка является экземпляром класса Account — то есть счетом пользователя. Выполняли приведение типа нашего объекта класса Account и метод getAmount , который возвращал сумму на этом счете. Дальше все это суммировали и возвращали итоговую сумму. Требовалось выполнить два действия:

Если не сделать проверку ( instanceof ) на принадлежность к классу Account , то на втором этапе возможен ClassCastException – то есть аварийное завершение программы. Поэтому такая проверка была обязательной.

С появлением Generics необходимость в проверке и приведении типа отпала:

Теперь метод принимает в качестве аргументов только список объектов класса Account . Это ограничение указано в самом методе, в его сигнатуре, программист просто не может передать никакой другой список — только список клиентских счетов.

Нам не нужно выполнять проверку типа элементов из этого списка: она подразумевается описанием типа у параметра метода (можно прочитать как список объектов класса Account ). И компилятор выдаст ошибку, если что-то пойдет не так — то есть если кто-то попробует передать в этот метод список объектов, отличных от класса Account .

Во второй строчке проверки необходимость тоже отпадала. Если потребуется, приведение типов ( casting ) будет сделано на этапе компиляции.

Принцип подстановки

Принцип подстановки Барбары Лисков – специфичное определение подтипа в объектно-ориентированном программировании. Идея Лисков о «подтипе» дает определение понятия замещения: если S является подтипом T , тогда объекты типа T в программе могут быть замещены объектами типа S без каких-либо изменений желательных свойств этой программы.

Тип Подтип
Number Integer
List<E> ArrayList<E>
Collection<E> List<E>
Iterable<E> Collection<E>

Примеры отношения тип/подтип

Вот пример использования принципа подстановки в Java:

Integer является подтипом Number , следовательно, переменной n типа Number можно присвоить значение, которое возвращает метод Integer.valueOf(42) .

Ковариантность, контравариантность и инвариантность

Сначала немного теории. Ковариантность — это сохранение иерархии наследования исходных типов в производных типах в том же порядке. Например, если Кошка — это подтип Животные, то Множество<Кошки> — это подтип Множество<Животные>. Следовательно, с учетом принципа подстановки можно выполнить такое присваивание:

Множество<Животные> = Множество<Кошки>

Контравариантность — это обращение иерархии исходных типов на противоположную в производных типах. Например, если Кошка — это подтип Животные , то Множество<Животные> — это подтип Множество<Кошки>. Следовательно, с учетом принципа подстановки можно выполнить такое присваивание:

Множество<Кошки> = Множество<Животные>

Инвариантность — отсутствие наследования между производными типами. Если Кошка — это подтип Животные, то Множество<Кошки> не является подтипом Множество<Животные> и Множество<Животные> не является подтипом Множество<Кошки>.

Массивы в Java ковариантны. Тип S[] является подтипом T[] , если S — подтип T . Пример присваивания:

Мы присвоили ссылку на массив строк переменной arr , тип которой – «массив объектов» . Если бы массивы не были ковариантными, нам бы это сделать не удалось. Java позволяет это сделать, программа скомпилируется и выполнится без ошибок.

Но если мы попытаемся изменить содержимое массива через переменную arr и запишем туда число 42, то получим ArrayStoreException на этапе выполнения программы, поскольку 42 является не строкой, а числом. В этом недостаток ковариантности массивов Java: мы не можем выполнить проверки на этапе компиляции, и что-то может сломаться уже в рантайме.

«Дженерики» инвариантны. Приведем пример:

Если взять список целых чисел, то он не будет являться ни подтипом типа Number , ни каким-либо другим подтипом. Он является только подтипом самого себя. То есть List <Integer> — это List<Integer> и ничего больше. Компилятор позаботится о том, чтобы переменная ints , объявленная как список объектов класса Integer, содержала только объекты класса Integer и ничего кроме них. На этапе компиляции производится проверка, и у нас в рантайме уже ничего не упадет.

Wildcards

Всегда ли Generics инварианты? Нет. Приведу примеры:

Это ковариантность. List<Integer> — подтип List<? extends Number>

Это контравариантность. List<Number> является подтипом List<? super Integer> .

Запись вида «? extends . » или «? super . » — называется wildcard или символом подстановки, с верхней границей ( extends ) или с нижней границей ( super ). List<? extends Number> может содержать объекты, класс которых является Number или наследуется от Number . List<? super Number> может содержать объекты, класс которых Number или у которых Number является наследником (супертип от Number ).

Запись вида T2 <= T1 означает, что набор типов описываемых T2 является подмножеством набора типов описываемых T1

т.е.
Number <=? extends Object
? extends Number <=? extends Object
и
? super Object <=? super Number

Пара задачек для проверки знаний:

1. Почему в примере ниже compile-time error? Какое значение можно добавить в список nums ?

2. Почему нельзя получить элемент из списка ниже?

Нельзя прочитать элемент из контейнера с wildcard ? super , кроме объекта класса Object

The Get and Put Principle или PECS (Producer Extends Consumer Super)

Особенность wildcard с верхней и нижней границей дает дополнительные фичи, связанные с безопасным использованием типов. Из одного типа переменных можно только читать, в другой — только вписывать (исключением является возможность записать null для extends и прочитать Object для super ). Чтобы было легче запомнить, когда какой wildcard использовать, существует принцип PECS — Producer Extends Consumer Super.

  • Если мы объявили wildcard с extends, то это producer. Он только «продюсирует», предоставляет элемент из контейнера, а сам ничего не принимает.
  • Если же мы объявили wildcard с super — то это consumer. Он только принимает, а предоставить ничего не может.

Метод осуществляет копирование элементов из исходного списка src в список dest . src — объявлен с wildcard ? extends и является продюсером, а dest — объявлен с wildcard ? super и является потребителем. Учитывая ковариантность и контравариантность wildcard, можно скопировать элементы из списка ints в список nums :

Если же мы по ошибке перепутаем параметры метода copy и попытаемся выполнить копирование из списка nums в список ints , то компилятор не позволит нам это сделать:

<?> и Raw типы

Ниже приведен wildcard с неограниченным символом подстановки. Мы просто ставим <?> , без ключевых слов super или extends :

На самом деле такой «неограниченный» wildcard все-таки ограничен, сверху. Collection<?> — это тоже символ подстановки, как и » ? extends Object «. Запись вида Collection<?> равносильна Collection<? extends Object> , а значит — коллекция может содержать объекты любого класса, так как все классы в Java наследуются от Object – поэтому подстановка называется неограниченной.

Если мы опустим указание типа, например, как здесь:

то, говорят, что ArrayList — это Raw тип параметризованного ArrayList<T>. Используя Raw типы, мы возвращаемся в эру до дженериков и сознательно отказываемся от всех фич, присущих параметризованным типам.

Если мы попытаемся вызвать параметризованный метода у Raw типа, то компилятор выдаст нам предупреждение «Unchecked call». Если мы попытаемся выполнить присваивание ссылки на параметризованный тип Raw типу, то компилятор выдаст предупреждение «Unchecked assignment». Игнорирование этих предупреждений, как мы увидим позже, может привести к ошибкам во время выполнения нашего приложения.

Wildcard Capture

Попробуем теперь реализовать метод, выполняющий перестановку элементов списка в обратном порядке.

Ошибка компиляции возникла, потому что в методе reverse в качестве аргумента принимается список с неограниченным символом подстановки <?> .
<?> означает то же что и <? extends Object> . Следовательно, согласно принципу PECS, list – это producer . А producer только продюсирует элементы. А мы в цикле for вызываем метод set() , т.е. пытаемся записать в list . И поэтому упираемся в защиту Java, что не позволяет установить какое-то значение по индексу.

Что делать? Нам поможет паттерн Wildcard Capture . Здесь мы создаем обобщенный метод rev . Он объявлен с переменной типа T . Этот метод принимает список типов T , и мы можем сделать сет.

Теперь у нас все скомпилируется. Здесь произошел захват символа подстановки (wildcard capture). При вызове метода reverse(List<?> list) в качестве аргумента передается список каких-то объектов (например, строк или целых чисел). Если мы можем захватить тип этих объектов и присвоить его переменной типа X , то можем заключить, что T является X .

Более подробно о Wildcard Capture можно прочитать здесь и здесь.

Вывод

Если необходимо читать из контейнера, то используйте wildcard с верхней границей » ? extends «. Если необходимо писать в контейнер, то используйте wildcard с нижней границей » ? super «. Не используйте wildcard, если нужно производить и запись, и чтение.

Не используйте Raw типы! Если аргумент типа не определен, то используйте wildcard <?> .

Переменные типа

Когда мы записываем при объявлении класса или метода идентификатор в угловых скобках, например <T> или <E> , то создаем переменную типа. Переменная типа — это неквалифицированный идентификатор, который можно использовать в качестве типа в теле класса или метода. Переменная типа может быть ограничена сверху.

В этом примере выражение T extends Comparable<T> определяет T (переменную типа), ограниченную сверху типом Comparable<T> . В отличие от wildcard, переменные типа могут быть ограничены только сверху (только extends ). Нельзя записать super . Кроме того, в этом примере T зависит от самого себя, это называется recursive bound — рекурсивная граница.

Вот еще пример из класса Enum:

Здесь класс Enum параметризован типом E, который является подтипом от Enum<E> .

Multiple bounds (множественные ограничения)

Multiple Bounds – множественные ограничения. Записывается через символ » & «, то есть мы говорим, что тип, представленный переменной типа T , должен быть ограничен сверху классом Object и интерфейсом Comparable .

Запись Object & Comparable<? super T&gt образует тип пересечения Multiple Bounds . Первое ограничение — в данном случае Object — используется для erasure , процесса затирания типов. Его выполняет компилятор на этапе компиляции.

Вывод

Переменная типа может быть ограничена только сверху одним или несколькими типами. В случае множественного ограничения левая граница (первое ограничение) используется в процессе затирания (Type Erasure).

Type Erasure

Type Erasure представляет собой отображение типов (возможно, включая параметризованные типы и переменные типа) на типы, которые никогда не являются параметризованными типами или переменными типами. Мы записываем затирание типа T как |T| .

  • Затиранием параметризованного типа G<T1. Tn> является |G|
  • Затиранием вложенного типа T.C является |T|.C
  • Затиранием типа массива T[] является |T|[]
  • Затиранием переменной типа является затирание ее левой границы
  • Затиранием любого иного типа является сам этот тип
  • добавляет приведение типов для обеспечения type safety, если это необходимо
  • генерирует Bridge методы для сохранения полиморфизма
T (Тип) |T| (Затирание типа)
List< Integer>, List< String>, List< List< String>> List
List< Integer>[] List[]
List List
int int
Integer Integer
<T extends Comparable<T>> Comparable
<T extends Object & Comparable<? super T>> Object
LinkedCollection<E>.Node LinkedCollection.Node

Эта таблица показывает, во что превращаются разные типы в процессе затирания, Type Erasure.

На скриншоте ниже два примера программы:

Разница между ними в том, что слева происходит compile-time error, а справа все компилируется без ошибок. Почему?

В Java два разных метода не могут иметь одну и ту же сигнатуру. В процессе Type Erasure компилятор добавит bridge-метод public int compareTo(Object o) . Но в классе уже содержится метод с такой сигнатурой, что и вызовет ошибку во время компиляции.

Скомпилируем класс Name, удалив метод compareTo(Object o) , и посмотрим на получившийся байткод с помощью javap:

Видим, что класс содержит метод int compareTo(java.lang.Object) , хотя мы его удалили из исходного кода. Это и есть bridge метод, который добавил компилятор.

Reifiable типы

  • Примитивные типы (int, long, boolean)
  • Непараметризованные (необобщенные) типы (String, Integer)
  • Параметризованные типы, параметры которых представлены в виде unbounded wildcard (неограниченных символов подстановки) (List<?>, Collection<?>)
  • Raw (несформированные) типы (List, ArrayList)
  • Массивы, компоненты которых — Reifiable типы (int[], Number[], List<?>[], List[)

Почему информация об одних типах доступна, а о других нет? Дело в том, что из-за процесса затирания типов компилятором информация о некоторых типах может быть потеряна. Если она потерялась, то такой тип будет уже не reifiable. То есть она во время выполнения недоступна. Если доступна – соответственно, reifiable.

Решение не делать все обобщенные типы доступными во время выполнения — это одно из наиболее важных и противоречивых проектных решений в системе типов Java. Так сделали, в первую очередь, для совместимости с существующим кодом. За миграционную совместимость пришлось платить — полная доступность системы обобщенных типов во время выполнения невозможна.

  • Переменная типа (T)
  • Параметризованный тип с указанным типом параметра (List<Number>ArrayList<String>, List<List<String>>)
  • Параметризованный тип с указанной верхней или нижней границей (List<? extends Number>, Comparable<? super String>). Но здесь стоит оговориться: List<? extends Object>не reifiable, а List<?> — reifiable

И еще одна задачка. Почему в примере ниже нельзя создать параметризованный Exception?

Каждое catch выражение в try-catch проверяет тип полученного исключения во время выполнения программы (что равносильно instanceof), соответственно, тип должен быть Reifiable. Поэтому Throwable и его подтипы не могут быть параметризованы.

Unchecked Warnings

Компиляция нашего приложения может выдать так называемый Unchecked Warning — предупреждение о том, что компилятор не смог корректно определить уровень безопасности использования наших типов. Это не ошибка, а предупреждение, так что его можно пропустить. Но желательно все-так исправить, чтобы избежать проблем в будущем.

Heap Pollution

Как мы упомянули ранее, присваивание ссылки на Raw тип переменной параметризованного типа, приводит к предупреждению «Unchecked assignment». Если мы проигнорируем его, то возможна ситуация под названием » Heap Pollution » (загрязнение кучи). Вот пример:

В строке (1) компилятор предупреждает об «Unchecked assignment».

Нужно привести и другой пример «загрязнения кучи» — когда у нас используются параметризованные объекты. Кусок кода ниже наглядно показывает, что недопустимо использовать параметризованные типы в качестве аргументов метода с использованием Varargs . В данном случае параметр метода m – это List<String>… , т.е. фактически, массив элементов типа List<String> . Учитывая правило отображения типов при затирании, тип stringLists превращается в массив raw списков ( List[] ), т.е. можно выполнить присваивание Object[] array = stringLists; и после записать в array объект, отличный от списка строк (1), что вызовет ClassCastException в строке (2).

Рассмотрим еще один пример:

Java разрешает выполнить присваивание в строке (1). Это необходимо для обеспечения обратной совместимости. Но если мы попытаемся выполнить метод add в строке (2), то получим предупреждение Unchecked call — компилятор предупреждает нас о возможной ошибке. В самом деле, мы же пытаемся в список строк добавить целое число.

Reflection

Хотя при компиляции параметризованные типы подвергаются процедуре стирания (type erasure), кое-какую информацию мы можем получить с помощью Reflection.

  • Все reifiable доступны через механизм Reflection
  • Информация о типе полей класса, параметров методов и возвращаемых ими значений доступна через Reflection.

С появлением Generics класс java.lang.Class стал параметризованным. Рассмотрим вот этот код:

Переменная ints имеет тип List<Integer> и она содержит ссылку на объект типа ArrayList< Integer> . Тогда ints.getClass() вернёт объект типа Class<ArrayLis> , так как List<Integer> затирается в List . Объект типа Class<ArrayList> можно присвоить переменной k типа Class<? extends List> , согласно ковариантности символов подстановки? extends . А ArrayList.class возвращает объект типа Class<ArrayList> .

Вывод

Если информация о типе доступна во время выполнения программы, то такой тип называется Reifiable. К Reifiable типам относятся: примитивные типы, непараметризованные типы, параметризованные типы с неограниченным символом подстановки, Raw типы и массивы, элементы которых являются reifiable.

Игнорирование Unchecked Warnings может привести к «загрязнению кучи» и ошибкам во время выполнения программы.

Reflection не позволяет получить информацию о типе объекта, если он не Reifiable. Но Reflection позволяет получить информацию о типе возвращаемого методом значения, о типе аргументов метода и о типе полей класса.

Type Inference

Термин можно перевести как «Вывод типа». Это возможность компилятора определять (выводить) тип из контекста. Вот пример кода:

С появлением даймонд-оператора в Java 7 мы можем не указывать тип у ArrayList :

Компилятор выведет тип ArrayList из контекста – List<Integer> . Этот процесс и называется type inference .

  • Приведение (reduction)
  • Объединение (incorporation)
  • Разрешение (resolution)

Предположим у нас есть вот такой класс, который описывает связный список:

Результат обобщенного метода List.nil() может быть выведен из правой части:

Механизм выбора типа компилятором показывает, что аргумент типа для вызова List.nil() действительно String — это работает в JDK 7, все хорошо.

Выглядит разумно, что компилятор также должен иметь возможность вывести тип, когда результат такого вызова обобщенного метода передается другому методу в качестве аргумента, например:

В JDK 7 мы получили бы compile-time error. А в JDK 8 скомпилируется. Это и есть первая часть JEP-101, его первая цель — вывод типа в позиции аргумента. Единственная возможность осуществить это в версиях до JDK 8 — использовать явный аргумент типа при вызове обобщенного метода:

Вторая часть JEP-101 говорит о том, что неплохо бы выводить тип в цепочке вызовов обобщенных методов, например:

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

После выхода JEP 101 на StackOverflow появилось множество вопросов по теме. Программисты спрашивают, почему код, который выполнялся на 7-й версии, на 8-й выполняется иначе – или вообще не компилируется? Вот пример такого кода:

Посмотрим на байт-код после компиляции на JDK1.8:

Инструкция под номером 0 выполняет вызов метода g:()Ljava/lang/Object; Метод возвращает java.lang.Object . Далее, инструкция 3 производит приведение типа («кастинг») объекта, полученного на предыдущем шаге к типу массива java.lang.String , и инструкция 6 выполняет метод m:([Ljava/lang/String;) , что и напечатает в консоли «two».

А теперь байт-код после компиляции на JDK1.7 – то есть на Java 7:

Мы видим, что здесь нет инструкции checkcast , которую добавила Java 8, так что вызовется метод m:(Ljava/lang/Object;) , а в консоли напечатается «one». Checkcast – результат нового выведения типа, который был усовершенствован в Java 8.

Чтобы избежать таких проблем, Oracle выпустил руководство по переходу с JDK1.7 на JDK 1.8 в котором описаны проблемы, которые могут возникнуть при переходе на новую версию Java, и то, как эти проблемы можно решить.

Например если вы хотите, чтобы в коде выше после компиляции на Java 8 все работало так же, как и на Java 7, сделайте приведение типа вручную:

Заключение

На этом мой рассказ о Java Generics подходит к концу. Вот другие источники, которые помогут вам в освоении темы:

Java Generics Type Erasure byte code

Consider the following generic class that represents a node in a singly linked list:

Because the type parameter T is unbounded, the Java compiler replaces it with Object:

But after compilation with Java 1.7.0_11, when I opened it with any decompiler I can see the same code as like source code.

If Type-Erasure applied at compile then the byte code must not contain Generic information as shown above. Kindly clarify me.

NOTE: I am using JD-GUI as a decompiler to analyze the byte code

4 Answers 4

The bytecode contains meta information about the code itself, such as generic types (or variable names) — it does not mean it’s useable by the JVM.

The disassembled bytecode of your class looks like below (you can see it with javap -c Node.class ):

You can see that the methods and arguments generic types are there but the code itself refers to Object as expected due to the erasure process.

assylias's user avatar

The fact that the class is generic is retained. For instance, at runtime you can call

The next bit of code will return «T».

You can’t get the value of type parameters at runtime, but the JVM knows they’re there.

Erasure comes into play when you construct a instance.

Generic classes are always generic. But instances do not carry generic type information inside them. The compiler verifies that everything you’ve done agrees with the generic type and then inserts casts.

Generic type information is still saved in the bytecode, specifically in the signature information of the class members.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *