если поле типизировано дженериком как в байт коде будет представлен этот тип java
Пришел, увидел, обобщил: погружаемся в Java Generics
Java Generics — это одно из самых значительных изменений за всю историю языка Java. «Дженерики», доступные с Java 5, сделали использование Java Collection Framework проще, удобнее и безопаснее. Ошибки, связанные с некорректным использованием типов, теперь обнаруживаются на этапе компиляции. Да и сам язык Java стал еще безопаснее. Несмотря на кажущуюся простоту обобщенных типов, многие разработчики сталкиваются с трудностями при их использовании. В этом посте я расскажу об особенностях работы с Java Generics, чтобы этих трудностей у вас было поменьше. Пригодится, если вы не гуру в дженериках, и поможет избежать много трудностей при погружении в тему.
Работа с коллекциями
Предположим, банку нужно подсчитать сумму сбережений на счетах клиентов. До появления «дженериков» метод вычисления суммы выглядел так:
С появлением Generics необходимость в проверке и приведении типа отпала:
Во второй строчке проверки необходимость тоже отпадала. Если потребуется, приведение типов ( casting ) будет сделано на этапе компиляции.
Принцип подстановки
Тип | Подтип |
Number | Integer |
List | ArrayList |
Collection | List |
Iterable | Collection |
Примеры отношения тип/подтип
Вот пример использования принципа подстановки в Java:
Ковариантность, контравариантность и инвариантность
Но если мы попытаемся изменить содержимое массива через переменную arr и запишем туда число 42, то получим ArrayStoreException на этапе выполнения программы, поскольку 42 является не строкой, а числом. В этом недостаток ковариантности массивов Java: мы не можем выполнить проверки на этапе компиляции, и что-то может сломаться уже в рантайме.
«Дженерики» инвариантны. Приведем пример:
Wildcards
Всегда ли Generics инварианты? Нет. Приведу примеры:
Это ковариантность. List — подтип List
extends B — символ подстановки с указанием верхней границы super B — символ подстановки с указанием нижней границы где B — представляет собой границу 2. Почему нельзя получить элемент из списка ниже? The Get and Put Principle или PECS (Producer Extends Consumer Super)Особенность wildcard с верхней и нижней границей дает дополнительные фичи, связанные с безопасным использованием типов. Из одного типа переменных можно только читать, в другой — только вписывать (исключением является возможность записать null для extends и прочитать Object для super ). Чтобы было легче запомнить, когда какой wildcard использовать, существует принцип PECS — Producer Extends Consumer Super. и Raw типыЕсли мы опустим указание типа, например, как здесь: Если мы попытаемся вызвать параметризованный метода у Raw типа, то компилятор выдаст нам предупреждение «Unchecked call». Если мы попытаемся выполнить присваивание ссылки на параметризованный тип Raw типу, то компилятор выдаст предупреждение «Unchecked assignment». Игнорирование этих предупреждений, как мы увидим позже, может привести к ошибкам во время выполнения нашего приложения. Wildcard CaptureПопробуем теперь реализовать метод, выполняющий перестановку элементов списка в обратном порядке. Более подробно о Wildcard Capture можно прочитать здесь и здесь. ВыводПеременные типаВот еще пример из класса Enum: Multiple bounds (множественные ограничения)ВыводПеременная типа может быть ограничена только сверху одним или несколькими типами. В случае множественного ограничения левая граница (первое ограничение) используется в процессе затирания (Type Erasure). Type ErasureНа скриншоте ниже два примера программы: Разница между ними в том, что слева происходит compile-time error, а справа все компилируется без ошибок. Почему? Reifiable типыПочему информация об одних типах доступна, а о других нет? Дело в том, что из-за процесса затирания типов компилятором информация о некоторых типах может быть потеряна. Если она потерялась, то такой тип будет уже не reifiable. То есть она во время выполнения недоступна. Если доступна – соответственно, reifiable. Решение не делать все обобщенные типы доступными во время выполнения — это одно из наиболее важных и противоречивых проектных решений в системе типов Java. Так сделали, в первую очередь, для совместимости с существующим кодом. За миграционную совместимость пришлось платить — полная доступность системы обобщенных типов во время выполнения невозможна. И еще одна задачка. Почему в примере ниже нельзя создать параметризованный Exception? Каждое catch выражение в try-catch проверяет тип полученного исключения во время выполнения программы (что равносильно instanceof), соответственно, тип должен быть Reifiable. Поэтому Throwable и его подтипы не могут быть параметризованы. Unchecked WarningsКомпиляция нашего приложения может выдать так называемый Unchecked Warning — предупреждение о том, что компилятор не смог корректно определить уровень безопасности использования наших типов. Это не ошибка, а предупреждение, так что его можно пропустить. Но желательно все-так исправить, чтобы избежать проблем в будущем. Heap PollutionКак мы упомянули ранее, присваивание ссылки на Raw тип переменной параметризованного типа, приводит к предупреждению «Unchecked assignment». Если мы проигнорируем его, то возможна ситуация под названием » Heap Pollution » (загрязнение кучи). Вот пример: В строке (1) компилятор предупреждает об «Unchecked assignment». Рассмотрим еще один пример: Java разрешает выполнить присваивание в строке (1). Это необходимо для обеспечения обратной совместимости. Но если мы попытаемся выполнить метод add в строке (2), то получим предупреждение Unchecked call — компилятор предупреждает нас о возможной ошибке. В самом деле, мы же пытаемся в список строк добавить целое число. ReflectionХотя при компиляции параметризованные типы подвергаются процедуре стирания (type erasure), кое-какую информацию мы можем получить с помощью Reflection. С появлением Generics класс java.lang.Class стал параметризованным. Рассмотрим вот этот код: ВыводЕсли информация о типе доступна во время выполнения программы, то такой тип называется Reifiable. К Reifiable типам относятся: примитивные типы, непараметризованные типы, параметризованные типы с неограниченным символом подстановки, Raw типы и массивы, элементы которых являются reifiable. Игнорирование Unchecked Warnings может привести к «загрязнению кучи» и ошибкам во время выполнения программы. Reflection не позволяет получить информацию о типе объекта, если он не Reifiable. Но Reflection позволяет получить информацию о типе возвращаемого методом значения, о типе аргументов метода и о типе полей класса. Type InferenceТермин можно перевести как «Вывод типа». Это возможность компилятора определять (выводить) тип из контекста. Вот пример кода: С появлением даймонд-оператора в Java 7 мы можем не указывать тип у ArrayList : Предположим у нас есть вот такой класс, который описывает связный список: Результат обобщенного метода 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: А теперь байт-код после компиляции на JDK1.7 – то есть на Java 7: Чтобы избежать таких проблем, Oracle выпустил руководство по переходу с JDK1.7 на JDK 1.8 в котором описаны проблемы, которые могут возникнуть при переходе на новую версию Java, и то, как эти проблемы можно решить. Например если вы хотите, чтобы в коде выше после компиляции на Java 8 все работало так же, как и на Java 7, сделайте приведение типа вручную: ЗаключениеНа этом мой рассказ о Java Generics подходит к концу. Вот другие источники, которые помогут вам в освоении темы: Какова концепция стирания в генерики в Java?какова концепция стирания в дженериках на Java? 8 ответовэто, конечно, делает больше, чем просто стирание; все автоматические вещи компилятор делается здесь. Например, также вставляются конструкторы по умолчанию, новый foreach-style for петли расширяются до регулярных for петли и т. д. Приятно видеть мелочи, которые происходит автоматически. чтобы завершить уже очень полный ответ Джона Скита, вы должны реализовать концепцию стирания типа происходит от потребности совместимость с предыдущими версиями Java. первоначально представленный на EclipseCon 2007 (больше не доступен), совместимость включала эти точки: есть предложения для большего овеществления. Reify being «рассматривает абстрактное понятие как реальное», где языковые конструкции должны быть понятия, а не только синтаксический сахар. механизм дженерики в языке обеспечивает проверку типов во время компиляции (статическую), но можно победить этот механизм с непроверенными приведениями. обычно это не проблема, так как компилятор выдает предупреждения обо всех таких непроверенных операциях. однако бывают случаи, когда одной статической проверки типа недостаточно, например: обновление июля 2012 года, почти четыре года спустя:
чтобы облегчить взаимодействие с неродовым устаревшим кодом, также можно использовать стирание параметризованного типа в качестве типа. Такой тип называется raw тип (Спецификация Языка Java 3/4.8). Разрешение типа raw также обеспечивает обратную совместимость исходного кода. в соответствии с этим, следующие версии java.util.Iterator класс как двоичный, так и исходный код обратно совместимы: стирание, буквально означает, что информация о типе, присутствующая в исходном коде, стирается из скомпилированного байт-кода. Давайте разберемся в этом с помощью некоторого кода. Если вы скомпилируете этот код, а затем декомпилируете его с помощью декомпилятора Java, вы получите что-то вроде этого. обратите внимание, что декомпилированный код не содержит следов информации о типе, присутствующей в исходном исходном коде. дополняя уже дополненный ответ Джона Скита. было упомянуто, что реализация дженериков через стирание приводит к некоторым раздражающим ограничениям (например, нет new T[42] ). Было также упомянуто, что основной причиной для этого была обратная совместимость в байт-коде. Это также (в основном) верно. Сгенерированный байт-код-target 1.5 несколько отличается от просто de-sugared casting-target 1.4. Технически, это возможно (через огромный обман), чтобы получить доступ к экземплярам универсального типа во время, доказывая, что в байт-коде действительно что-то есть. стирание может быть неудобно, когда вы хотите озорничать во время выполнения, но он предлагает большую гибкость для разработчиков компилятора. Я предполагаю, что это часть того, почему он не уходит в ближайшее время. Это означает, что дженерики Java-это не что иное, как синтаксический сахар и не предлагают никакого улучшения производительности для типов значений, которые требуют бокса/распаковки при передаче по ссылке. есть хорошие объяснения. Я только добавляю пример, чтобы показать, как стирание типа работает с декомпилятором. декомпилированный код из байт-кода, Generic programming вводится в версии java 1.5 что преимущества родового?
Стирание типов в Java ОбъясненоУзнайте о важном механизме в том, как Java обрабатывает удаление общего типа. 1. ОбзорВ этой краткой статье мы обсудим основы важного механизма в универсальных приложениях Java, известного как стирание типов. 2. Что Такое Стирание Типа?Удаление типов можно объяснить как процесс применения ограничений типа только во время компиляции и удаления информации о типе элемента во время выполнения. Компилятор заменяет несвязанный тип E фактическим типом Объекта : Поэтому компилятор обеспечивает типобезопасность нашего кода и предотвращает ошибки во время выполнения. 3. Типы стирания типовСтирание типов может происходить на уровне классов (или переменных) и методов. 3.1. Стирание типа КлассаДавайте реализуем Стек с использованием массива: При компиляции компилятор заменяет параметр несвязанного типа E на Объект : В случае, когда параметр типа E привязан: Компилятор заменит параметр связанного типа E первым связанным классом, Сопоставимым в этом случае : 3.2. Стирание Типа МетодаДавайте рассмотрим метод отображения содержимого любого заданного массива: При компиляции компилятор заменяет параметр типа E на Объект : Для параметра типа связанного метода: У нас будет параметр типа E удален и заменен на Сопоставимый: 4. Крайние СлучаиИногда в процессе стирания типов компилятор создает синтетический метод для различения похожих методов. Они могут быть получены из сигнатур методов, расширяющих один и тот же первый связанный класс. Теперь давайте рассмотрим следующий код: После стирания типа у нас есть: Поэтому неудивительно, что попытка вставить строку и присвоить Целое число вызывает исключение ClassCastException из приведения, вставленного во время push компилятором. 4.1. Мостовые методыЧтобы решить описанный выше пограничный случай, компилятор иногда создает метод моста. Это синтетический метод, созданный компилятором Java при компиляции класса или интерфейса, который расширяет параметризованный класс или реализует параметризованный интерфейс, где сигнатуры методов могут немного отличаться или быть неоднозначными. В приведенном выше примере компилятор Java сохраняет полиморфизм универсальных типов после удаления, гарантируя отсутствие несоответствия сигнатур метода между Целым стеком s push(Целое число) методом и Стеком s push(объект) методом. Следовательно, компилятор создает здесь метод моста: Следовательно, Стек метод класса push после удаления типа делегирует исходный push метод Целочисленного стека класса. 5. ЗаключениеВ этом учебном пособии мы обсудили концепцию стирания типов с примерами в переменных и методах параметров типов. Вы можете прочитать больше об этих концепциях: Стирание типов дженериков в JavaЯ понимаю, что параметр типа T стирается во время выполнения, но тогда почему параметр типа ob выжившие во время выполнения? 5 ответоврассмотрим следующий пример: но разве я не должен получить объект.toString ()? стирания типа происходит. Generics-это система проверки типа времени компиляции. Во время выполнения вы все еще получаете класс (это информация о типе времени выполнения). Связанная документация по стиранию типа говорит (частично)
заменить все параметры типа в универсальных типах с их границами или объектом, если параметры типа неограниченны. Таким образом, созданный байт-код содержит только обычные классы, интерфейсы и методы. кроме того, о вашем вопросе о том, почему getClass() запоминает аргумент типа: это не так. Все, что он делает, это определяет класс контента. Для пример: если теперь запустить следующий фрагмент. потому что при компиляции класс Gen имеет объект ob; дженерики исчезают из конечного продукта. Угловые скобки играют роль только во время компиляции, во время статической проверки типа. Это то, что компилятор может сделать для вас, чтобы дать вам более душевное спокойствие, чтобы заверить вас, что вы используете правильно сборов и других видах переутверждаются. помните, что выходит эффективно класс Gen поскольку мое первое воздействие дженериков было с C#, потребовалось время, чтобы понять, какой тип стирания находится в java. но после лучшего понимания Java generics я понял, что в моем вопросе я смешиваю 2 отдельные темы : Generics и Reflection.
и getClass() не возвращает тип ссылки, но фактический объект, на который ссылается ссылка. Основы дженериков JavaКраткое введение в основы Java-дженериков. 1. введениеДженерики Java были введены в JDK 5.0 с целью уменьшения ошибок и добавления дополнительного уровня абстракции над типами. Эта статья представляет собой краткое введение в дженерики в Java, цель, стоящую за ними, и как их можно использовать для улучшения качества нашего кода. Дальнейшее чтение:Ссылки на методы в JavaПолучение полей из класса Java с помощью отражения2. Потребность в дженерикахДавайте представим себе сценарий, в котором мы хотим создать список на Java для хранения Integer ; у нас может возникнуть соблазн написать: Удивительно, но компилятор будет жаловаться на последнюю строку. Он не знает, какой тип данных возвращается. Компилятор потребует явного приведения: Было бы намного проще, если бы программисты могли выразить свое намерение использовать определенные типы, а компилятор мог бы обеспечить правильность такого типа. Это основная идея, лежащая в основе дженериков. Давайте изменим первую строку предыдущего фрагмента кода на: Добавляя оператор diamond<>, содержащий тип, мы сужаем специализацию этого списка только до Integer type, т. е. указываем тип, который будет храниться внутри списка. Компилятор может принудительно применить тип во время компиляции. В небольших программах это может показаться тривиальным дополнением, однако в больших программах это может значительно повысить надежность и облегчить чтение программы. 3. Общие методыУниверсальные методы-это те методы, которые написаны с одним объявлением метода и могут вызываться с аргументами разных типов. Компилятор обеспечит правильность любого используемого типа. Вот некоторые свойства универсальных методов: Пример определения универсального метода преобразования массива в список: Мы передаем функцию, которая преобразует массив с элементами типа T в список с элементами типа G. Примером может быть преобразование Целого числа в его строковое представление: Стоит отметить, что Oracle рекомендует использовать заглавную букву для представления общего типа и выбирать более описательную букву для представления формальных типов, например, в коллекциях Java T используется для типа, K для ключа, V для значения. 3.1. Ограниченные дженерикиКак упоминалось ранее, параметры типа могут быть ограничены. Ограниченный означает ” ограниченный “, мы можем ограничить типы, которые могут быть приняты методом. Например, мы можем указать, что метод принимает тип и все его подклассы (верхняя граница) или тип все его суперклассы (нижняя граница). Чтобы объявить верхний ограниченный тип, мы используем ключевое слово extends после типа, за которым следует верхняя граница, которую мы хотим использовать. Например: Ключевое слово extends используется здесь для обозначения того, что тип T расширяет верхнюю границу в случае класса или реализует верхнюю границу в случае интерфейса. 3.2. Множественные границыТип также может иметь несколько верхних границ следующим образом: 4. Использование Подстановочных Знаков С ДженерикамиПодстановочные знаки представлены знаком вопроса в Java ” ? ” и они используются для обозначения неизвестного типа. Подстановочные знаки особенно полезны при использовании дженериков и могут использоваться в качестве типа параметра, но сначала следует рассмотреть важное примечание. Известно, что Объект является ли супертип всех классов Java, однако, коллекцией Объект не является супертипом какой-либо коллекции. Например, List не является супертипом List и назначение переменной типа List переменной типа List приведет к ошибке компилятора. Это делается для предотвращения возможных конфликтов, которые могут возникнуть, если мы добавим разнородные типы в одну и ту же коллекцию. То же правило применяется к любой коллекции типа и его подтипов. Рассмотрим этот пример: Теперь этот метод будет работать с типом Building и всеми его подтипами. Это называется верхним ограниченным подстановочным знаком, где тип Building является верхней границей. 5. Тип СтиранияДженерики были добавлены в Java для обеспечения безопасности типов и для того, чтобы дженерики не вызывали накладных расходов во время выполнения, компилятор применяет процесс, называемый стирание типов для дженериков во время компиляции. Это пример стирания типов: При стирании типа неограниченный тип T заменяется на Object следующим образом: Если тип ограничен, то во время компиляции он будет заменен на связанный: изменится после компиляции: 6. Универсальные и примитивные типы данныхОграничение универсалий в Java заключается в том, что параметр типа не может быть примитивным типом. Например, следующее не компилируется: В качестве примера давайте рассмотрим метод add списка: Подпись метода add является: И будет скомпилирован для: Однако Java предоставляет упакованные типы для примитивов, а также автобоксы и распаковку, чтобы развернуть их: Итак, если мы хотим создать список, который может содержать целые числа, мы можем использовать оболочку: Скомпилированный код будет эквивалентен: 7. ЗаключениеДженерики Java являются мощным дополнением к языку Java, поскольку они облегчают работу программиста и менее подвержены ошибкам. Универсальные алгоритмы обеспечивают корректность типов во время компиляции и, самое главное, позволяют реализовывать универсальные алгоритмы, не вызывая дополнительных накладных расходов для наших приложений.
|