Java 8 обобщения

Цикл статей «Учебник Java 8».

Следующая статья — «Java 8 исключения».
Предыдущая статья — «Java 8 ещё раз о перегрузке методов».

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

Содержание

Простой класс Lair
Обобщённая версия класса Lair
Соглашение об именовании переменных типа
Создание экземпляра обобщённого типа и обращение к нему
Бриллиантовая операция (Diamond operator)
Несколько параметров типа
Параметризованный тип
Сырой тип (Raw type)
Сообщения об ошибках “unchecked”
Обобщённые методы
Ограниченные параметры типа
Обобщения, наследование и дочерние типы
Выведение типов
Выведение типов и обобщённый методы
Выведение типов и создание экземпляра обобщённого класса
Выведение типа и обобщённые конструкторы обобщённых и необобщённых классов
Целевые типы
Подстановочный символ (wildcard)
Подстановочный символ, ограниченный сверху (Upper bounded wildcard)
Неограниченный подстановочный символ (Unbounded wildcard)
Ограниченный снизу подстановочный символ (Lower bound Wildcard)
Подстановочные символы и дочерние типы
Захват символа подстановки (Wildcard Capture) и вспомогательные методы
Руководство по использованию подстановочного символа
Стирание типа (Type Erasure)
Стирание типа в обобщённых типах
Стирание типа в обобщённых методах
Влияние стирания типа и методы-мосты (bridge methods)
— —Методы-мосты (Bridge Methods)
Загрязнение кучи (Heap pollution)
Потенциальные уязвимости методов с произвольным числом параметров с нематериализуемыми формальными параметрами
Подавление предупреждений для методов с произвольным количеством параметров с нематериализуемыми формальными параметрами
Ограничения обобщений
Нельзя создавать экземпляры обобщённых типов с примитивными типами в качестве аргументов типа.
Нельзя создавать экземпляры параметров типа
Нельзя объявлять статические поля с типом параметра типа
Нельзя использовать приведения типа или instanceof с параметризованными типами
Невозможно создавать массивы параметризованных типов
Нельзя создавать, ловить (catch) или бросать (throw) объекты параметризованных типов
Нельзя перегружать метод так, чтобы формальные параметры типа стирались в один и тот же сырой тип

Простой класс Lair

Посмотрите следующий код:

Так как класс работает с Object , вы можете свободно поместить в жилище Lair  абсолютно любой класс. Нет никакого способа проверки использования класса во время компиляции. Одна часть кода может поселить в жилище гоблина Goblin , а другая попытаться вытащить джинна Genie , что приведёт к ошибке во время выполнения.

Обобщённая версия класса Lair

Обобщённый класс объявляется вот так:

Секция параметра типа, заключённая в угловые скобки <> , следует за именем класса. Она определяет параметры типа (parameter types, также называются переменными типа type variables) T1, T2 … Tn.

Чтобы класс Lair  использовал обобщение, нужно сделать объявление обобщённого класса путём замены class Lair на class Lair<T> , что создаст переменную типа T , которую можно использовать в любом месте класса Lair.

С этими изменениями код класса Lair  станет таким:

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

Соглашение об именовании переменных типа

По соглашению переменные типа именуются одной буквой в верхнем регистре. Это сильно отличается от соглашения об именовании переменных, классов и интерфейсов. Без такого отличия было бы трудно отличить переменную типа от класса и интерфейса.
Наиболее часто используемые имена для параметров типа:

  • E — элемент (Element, обширно используется Java Collections Framework)
  • K — Ключ
  • N — Число
  • T — Тип
  • V — Значение
  • S, U, V и т. п. — 2-й, 3-й, 4-й типы

 

Создание экземпляра обобщённого типа и обращение к нему

При обращении к обобщённому типу нужно заменить параметры типа на конкретные классы или интерфейсы, например Goblin :

Вы можете думать, что обращение к параметризованному типу похоже на обычный вызов метода, но вместо передачи аргумента в метод вы передаёте аргумент типа (type argument), например Goblin  в класс Lair  в данном случае.

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

Как и любое другое объявление переменной Lair<Goblin> goblinLair НЕ создаёт экземпляр класса Lair . Такой код просто объявляет переменную goblinLair  как Lair  Goblin -ов.

Обращение к обобщённому типу обычно называется параметризованным типом (parameterized type).

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

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

Бриллиантовая операция (Diamond operator)

Начиная с Java 7 существует также бриллиантовая операция (diamond operator), которая позволяет указывать пустые аргументы типа <>  там, где компилятор может вывести тип из контекста:

Несколько параметров типа

Обобщённый тип может иметь несколько параметров типа:

Параметризованный тип

Вы можете также заменить параметр типа на параметризованный тип:

Обобщённые интерфейсы объявляются схожим с обобщёнными классами способом.

Сырой тип (Raw type)

Сырой тип (raw type) — это имя обобщённого класса или интерфейса без аргументов типа (type arguments).

Например, параметризованный тип создаётся так:

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

Поэтому Lair  — это сырой тип обобщённого типа Lair<T> . Однако необобщённый класс или интерфейс НЕ являются сырыми типами.

Вы можете часто увидеть использование сырых типов в старом коде, поскольку многие классы, например коллекции) до Java 5 были необобщёнными. Когда вы используете сырые типы, вы по сути получаете то же самое поведение, которое было до введения обобщений в Java.

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

Но если вы попытаетесь присвоить параметризованному типу сырой тип, то будет предупреждение (warning):

Вы также получите предупреждение (warning), если попытаетесь вызвать обобщённый метод в сыром типе:

Предупреждение показывает, что сырой тип обходит проверку обобщённого типа, что откладывает обнаружение ошибки на выполнение программы.

Сообщения об ошибках “unchecked”

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

Термин “unchecked” означает непроверенные, то есть компилятор не имеет достаточного количества информации для обеспечения безопасности типов. По умолчанию этот вид предупреждений выключен, поэтому компилятор даёт подсказку. Чтобы видеть все “unchecked” предупреждения нужно перекомпилировать код с опцией -Xlint:unchecked :

Предупреждения будут такого вида:

Чтобы полностью отключить подобные предупреждения нужно перекомпилировать с опцией -Xlint:-unchecked . Можно также использовать аннотацию @SuppressWarnings("unchecked") , чтобы отключить эти предупреждения для поля, метода, параметра, конструктора или локальной переменной.

Обобщённые методы

Обобщённые методы похожи на обобщённые классы, но параметры типа относятся к методу, а не к классу. Допустимо делать обобщёнными статические и нестатические методы, а также конструкторы.

Синтаксис обобщённого метода включает параметры типа внутри угловых скобок, которые указываются перед возвращаемым типом.

Пример вызова обобщённого метода:

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

Более подробно выведение типа будет описано ниже.

Ограниченные параметры типа

В некоторых случаях имеет смысл ограничить  типы, которые можно использовать в качестве аргументов в параметризованных типах. Например, в жилище Lair  могут жить только наследники класса Monster . Подобное ограничение можно сделать с помощью ограниченного параметра типа (bounded type parameters).

Чтобы объявить ограниченный параметр типа нужно после имени параметра указать ключевое слово extends, а затем указать верхнюю границу (upper bound), которой в данном примере является класс Monster. В этом контексте extends  означает как extends , так и implements.

В этом примере мы ограничили возможные типы, которые можно использовать в параметризованных классах Lair, наследниками класса Monster. Если попытаться указать, например, Lair<Integer> , то возникнет ошибка компиляции. Плюс в дополнение мы получили возможность вызывать в обобщённом классе методы класса Monster( inhabitant.doSomething(); ).

Можно указать несколько верхних границ, перечисляя их через символ «&», но при этом только один класс может быть указан в списке верхних границ, и он должен стоять первым:

Аналогичным образом можно создавать обобщённые методы с ограничением:

Обобщения, наследование и дочерние типы

Как вы уже знаете, можно присвоить объекту одного типа объект другого типа, если эти типы совместимы. Например, вы можете присвоить объект типа Integer  переменной типа Object , так как Object  является одним из супертипов Integer -а:

В объектно-ориентированной терминологии это называется связью «является» (“is a”). Так как Integer  является Object -ом, то такое присвоение разрешено. Но Integer  также является и Number , поэтому следующий код тоже корректен:

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

Теперь рассмотрим метод:

Какой тип аргумента он будет принимать? Если посмотрите на сигнатуру, то вы можете увидеть, что он принимает один аргумент с типом Box<Number>. Но что это означает? Можете ли вы передать Box<Integer>  или Box<Double> , как вы могли бы ожидать? Нет, не можете, так как Box<Integer>  и Box<Double>  не являются потомками Box<Number>.

Это частое недопонимание принципов работы обобщений, и это важно знать.

Запомните: Для двух типов A  и B  (например, Number  и Integer ), MyClass<A>  не имеет никакой связи или родства с MyClass<B> , независимо от того, как A  и B  связаны между собой. Общий родитель MyClass<A>  и MyClass<B>  — это Object.

Информация о способах создания связи подобной потомок-родитель между двумя обобщёнными типами, когда параметры типа связаны, смотрите раздел «Подстановочные символы и дочерние типы».

Вы можете указать обобщённый класс или интерфейс в качестве родительского для своего класса или интерфейса. Связь между параметрами типа одного класса или интерфейса и другого определяются ключевыми словами extends  и implements .

Для классов коллекций, например, ArrayList<E> implements List<E> , и List<E> extends Collection<E> . Также ArrayList<String>  является дочерним типом для List<String> , который является дочерним типом Collection<String> . Наследование между типами сохраняется, пока вы не меняете аргумент типа.

Давайте представим, что мы хотим определить свой собственный интерфейс списка PayloadList , который связывает необязательное значение P  с каждым элементом. Объявление может выглядеть так:

Следующие параметризованные типы являются дочерними типами для List<String> , но не связаны между собой:

  • PayloadList<String,String>
  • PayloadList<String,Integer>
  • PayloadList<String,Exception>

Выведение типов

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

В приведённом ниже примере выведение типов определяет, что второй аргумент, передаваемый в метод pick  имеет тип Serializable :

Выведение типов и обобщённый методы

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

Этот код выведет в консоль следующее:

Обобщённый метод addLair  объявляет один параметр типа U . В большинстве случаев компилятор Java может вывести параметры типа вызова обобщённого метода, в результате вам чаще всего вовсе не обязательно их указывать. Например, чтобы вызвать обобщённый метод addBox , вы можете указать параметры типа так:

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

Выведение типов и создание экземпляра обобщённого класса

Вы можете заменить аргументы типа, необходимые для вызова конструктора обобщённого класса пустым множеством параметров типа ( <> ), так как компилятор может вывести аргументы типа из контекста. Эта пара угловых скобок называется бриллиантовой операцией (diamond operator).

Рассмотрим следующее объявление переменной:

Вы можете заменить параметризованный тип конструктора пустыми угловыми скобками ( <> ):

Обратите внимание, что для того чтобы воспользоваться выведением типов при создании экземпляра обобщённого класса, вы должны использовать бриллиантовую операцию (diamond operator). В примере ниже компилятор сгенерирует предупреждение unchecked conversion warning, так как конструктор HashMap()  обращается к сырому типу HashMap, а не к Map<String, List<String>> :

Выведение типа и обобщённые конструкторы обобщённых и необобщённых классов

Конструкторы могут быть обобщёнными как в обобщённых, так и в необобщённых классах. Рассмотрим пример:

Рассмотрим создание экземпляра класса MyClass :

Эта инструкция создаёт экземпляр параметризованного типа MyClass<Integer> . Инструкция явно указывает Integer  в качестве формального параметра типа X  обобщённого класса MyClass<X> . Обратите внимание, что конструктор этого обобщённого класса содержит параметр типа T . Компилятор выводит тип String  для этого формального параметра T , так как фактически переданный аргумент является экземпляром класса String.

Компилятор Java 7 и более поздней версии может вывести аргументы типа создаваемого экземпляра обобщённого класса с помощью бриллиантовой операции (diamong operator). Пример:

В этом примере компилятор выводит Integer  для параметра типа X  обобщённого класса MyClass<X> . Он выводит тип String  для параметра T  обобщённого конструктора обобщённого класса.

Важно запомнить, что алгоритм выведения типа использует только аргументы вызова, целевые типы и возможно очевидный ожидаемый возвращаемый тип для выведения типов. Алгоритм выведения не использует последующий код программы.

Целевые типы

Компилятор Java пользуется целевыми типами для вывода параметров типа вызова обобщённого метода. Целевой тип выражения — это тип данных, который компилятор Java ожидает в зависимости от того, в каком месте находится выражение. Рассмотрим метод Collections.emptyList() , который объявлен так:

Рассмотрим следующую инструкцию присвоения:

Эта инструкция ожидает экземпляр List<String> . Этот тип данных является целевым типом. Поскольку метод emptyList  возвращает значение типа List<T> , компилятор выводит, что аргумент типа T  будет типом String . Это работает как в Java 7, так и в Java 8. Вы также можете указывать аргумент типа напрямую:

Но в данном случае в этом нет необходимости. Это может быть необходимо в других случаях. Рассмотрим метод:

Представьте, что вы хотите вызвать метод processStringList  с пустым списком. В Java 7 следующий код не будет работать:

Компилятор Java 7 сгенерирует примерно такую ошибку:

Компилятору необходимо значение аргумента типа для T , и он начинает с Object . В результате вызов Collections.emptyList  возвращает тип List<Object> , который несовместим с методом processStringList . Таким образом в Java 7 вы должны указать аргумент типа так:

В Java 8 в этом больше нет необходимости. Термин целевой тип расширен и включает аргументы методов. В этом случае processStringList -у необходим аргумент типа List<String>  Метод Collections.emptyList  возвращает значение типа List<T> , компилятор выводит аргумент типа T  как String , используя целевой тип List<String> . Таким образом в Java 8 следующая инструкция успешно скомпилируется:

Подстановочный символ (wildcard)

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

Подстановочный символ, ограниченный сверху (Upper bounded wildcard)

Вы може использовать подстановочный символ, ограниченный сверху, чтобы ослабить ограничения переменной. Например, если вы хотите написать метод, который работает с List<Integer> , List<Double>  и List<Number> , вы можете достичь этого с помощью ограниченного сверху подстановочного символа.

Чтобы объявить ограниченный сверху подстановочный символ, используйте символ вопроса «?», с последующим клчевым словом extends , с последующим ограничением сверху. Запомните, что в этом контексте extends  означает как расширение класса, так и реализацию интерфейса.

Чтобы написать метод, который работает со списками Number  и дочерними типами от Number , например Integer , Double  и Float , вы можете указать List<? extends Number> . List<Number>  вводит более жёсткое ограничение, чем List<? extends Number> , потому что оно соответствует только списку типа Number , а  List<? extends Number>  соответствует списку типа Number  и спискам всех его подклассов.

Рассмотрим следующий метод process :

Ограниченный сверху подстановочный символ <? extends Monster> , где Monster  — любой тип, соответствует Monster  и любому подтипу Monster . Метод process  может обращаться к элементу списка как к типу Monster :

Любой метод класса или интерфейса Monster  может быть использован у elem .

Метод sumOfList  возвращает сумму чисел в списке:

Следующий код использует список Integer -ов и выведет: «sum = 6.0»:

Список Double -ов может использовать тот же самый метод. Следующий код выведет: «sum = 7.0»:

Неограниченный подстановочный символ (Unbounded wildcard)

Если просто использовать подстановочный символ, то получится подстановочный символ без ограничений. List<?> Означает список неизвестного типа.

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

  • Если вы пишете метод, который может быть реализован с помощью функциональности класса Object.
  • Когда код использует методы обобщённого класса, которые не зависят от параметра типа. Например, List.size()  или List.clear() . В реальности Class<?>  используется так часто, потому что большинство методов Class<T>  не зависят от T.

Рассмотрите следующий метод printList :

Цель метода printList  — вывод в консоль списка любого типа, но сейчас он её не выполняет, так как он может вывести в консоль только список объектов типа Object . Он не может принимать в качестве параметра List<Integer> , List<String> , List<Double>  и какие-либо ещё, так как они не являются дочерними типами для List<Object> . Чтобы сделать этот метод более общим, нужно использовать List<?> :

List<A>  является дочерним типом для List<?> для любого конкретного типа A , поэтому вы можете использовать printList  для вывода в консоль списков любого типа:

Заметка: Метод  Arrays.asList  конвертирует массив и возвращает список фиксированного размера.

Важно запомнить, что List<Object>  и List<?>  — это НЕ одно и то же. Вы можете вставить Object  или любой дочерний тип от Object  в List<Object> . Но вы можете вставить только null  в List<?> . Пункт «Руководство по использованию подстановочного символа» содержит информацию о выборе типа подстановочного символа, который следует использовать в конкретной ситуации.

Ограниченный снизу подстановочный символ (Lower bound Wildcard)

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

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

Допустим, что вы хотите написать метод, который добавляет объекты Integer  в список. Чтобы максимизировать гибкость, вы можете захотеть, чтобы метод работал с List<Integer> , List<Number>  и List<Object>  — всё, что может хранить экземпляры класса Integer .

Чтобы написать метод, который работает со списком Integer-ов и супертипами Integer-а (такими как Integer , Number  и Object ), вы можете указать List<? super Integer>. Вариант List<Integer>  более ограничен, чем List<? super Integer> , потому что он позволяет использовать только список объектов типа Integer , тогда как List<? super Integer>  соответствует спискам любого родительского класса от Integer  и списку Integer-ов.

Следующий код добавляет числа от 1 до 10 в конец списка:

Пункт «Руководство по использованию подстановочного символа» содержит советы, когда использовать ограниченный сверху подстановочный символ, а когда использовать ограниченный снизу подстановочный символ.

Подстановочные символы и дочерние типы

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

С данными обычными (необобщёнными) классами:

Имеет смысл написать вот такой код:

Этот пример показывает, что наследование следует правилу подчинённых типов: класс Goblin  является подклассом класса Monster, если он расширяет его. Это правило НЕ работает для обобщённых типов:

Если Integer  является дочерним типом для Number , то какая связь между List<Integer>  и List<Number> ?

Не смотря на то что Integer  является подтипом  Number , List<Integer>  не является подтипом List<Number>. Это разные типы. Общим предком для List<Number>  и List<Integer>  является List<?>.

Для того чтобы создать такую связь между этими классами, чтобы код мог иметь доступ к методам  Number  через элементы List<Integer> , используйте подстановочный символ:

Так как Integer  является дочерним типом от Number , и numList  является списком объектов типа Number , теперь существует связь между intList  (список объектов типа Integer ) и numList . Следующая диаграмма показывает связь между несколькими классами List , объявленными с ограниченными сверху подстановочными символами и ограниченными снизу подстановочными символами.

bounded wildcard hierarchy

 

Пункт «Руководство по использованию подстановочного символа» содержит больше информации об использовании ограниченных сверху подстановочны символов и ограниченных снизу подстановочных символов.

Захват символа подстановки (Wildcard Capture) и вспомогательные методы

В некоторых случаях компилятор может вывести тип подстановочного символа. Список может быть определён как List<?>, но при вычислении выражения компилятор выведет конкретный тип из кода. Этот сценарий называется захватом подстановочного символа.

В большинстве случаев вам нет нужды беспокоиться о захвате подстановочного символа, кроме случаев, когда вы видите фразу “capture of” в сообщении об ошибке.

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

В этом примере компилятор обрабатываем параметр i  как тип Object . Когда метод foo  вызывает List.set(int, E) , компилятор не может подтвердить тип объекта, который будет вставляться в список, и генерирует ошибку. Когда возникает этот тип ошибки, это обычно означает, что компилятор верит, что вы присваиваете неправильный тип переменной. Обобщения были добавлены в Java именно для этого — чтобы усилить безопасность типов во время компиляции.

При компиляции в JDK 8 кода, приведённого выше, компилятор выдаст следующее сообщение об ошибке:

В нашем же примере код пытается выполнить безопасную операцию, тогда как мы можем обойти ошибку компиляции? Вы можете исправить её написав приватный вспомогательный метод (private helper method), который захватывает подстановочный символ. В этом случае вы можете обойти проблему с помощь создания приватного вспомогательного метода fooHelper() :

Благодаря вспомогательному методу компилятор использует выведение типа для определения, что T  является CAP#1 ( захваченная переменная в вызове). Пример теперь успешно компилируется.

По соглашению вспомогательные методы обычно называются как originalMethodNameHelper .

Теперь рассмотрите более сложный пример, WildcardErrorBad :

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

Не смотря на то что List<Integer>  и List<Double>  оба удовлетворяют критерию List<? extends Number> , это совершенно неверно брать элемент из списка объектов типа Integer  и пытаться добавить его в список объектов типа Double .

При компиляции в Oracle JDK 8 компилятор выведет следующее сообщение об ошибке:

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

Руководство по использованию подстановочного символа

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

В этом обсуждении будет полезно думать о переменных, будто они представляют две функции:

  • Входная переменная. Предоставляет данные для кода. Для метода copy(src, dst) параметр src  предоставляет данные для копирования, поэтому он считается входной переменной.
  • Выходная переменная. Содержит данные для использования в другом месте. В примере с copy(src, dst)  параметр dst  принимает данные и является выходной переменной.

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

Руководство:

  • Входная переменная определяется с ограниченным сверху подстановочным символом, используя ключевое слово extends.
  • Выходная переменная определяется с ограниченным снизу подстановочным символом, используя ключевое слово super.
  • Если к входной переменной можно обращаться только используя методы класса Object, используйте неограниченный подстановочный символ.
  • Если переменная должна использоваться как входная и как выходная одновременно, то НЕ используйте подстановочный символ.

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

Список, объявленный как List<? extends ...>  может неформально считаться как только для чтения, но это не строгое ограничение. Предположим, что вы имеете следующие два класса:

Рассмотрите следующий код:

Так как List<EvenNumber>  является дочерним типом от List<? extends NaturalNumber> , то вы можете присвоить le  переменной ln . Но вы не можете использовать ln  для добавления NaturalNumber  в список объектов EvenNumber . Над списком возможны следующие операции:

  • Вы можете добавить в список null .
  • Вы можете вызвать clear.
  • Вы можете получить iterator  и вызвать remove.
  • Вы можете захватить подстановочный символ и записывать элементы, которые вы прочитали из списка.

Вы можете увидеть, что список List<? extends NaturalNumber>  НЕ является списком только для чтения, но вы можете считать его таким, потому что вы не можете добавить туда новый элемент или заменить существующий элемент в списке.

Стирание типа (Type Erasure)

Обобщения были введены в язык программирования Java для обеспечения более жёсткого контроля типов во время компиляции и для поддержки обобщённого программирования. Для реализации обобщения компилятор Java применяет стирание типа (type erasure) к:

  • Заменяет все параметры типа в обобщённых типах их границами или Object-ами, если параметры типа не ограничены. Сгенерированный байткод содержит только обычные классы, интерфейсы и методы.
  • Вставляет приведение типов где необходимо, чтобы сохранить безопасность типа.
  • Генерирует связующие методы, чтобы сохранить полиморфизм в расширенных (extended, наследующиеся от других) обобщённых типах.

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

Стирание типа в обобщённых типах

Во время процесса стирания типов компилятор Java стирает все параметры типа и заменяет каждый его ограничением, если параметр типа ограничен, либо Object -ом, если параметр типа неограничен.

Рассмотрим следующий обобщённый класс, который представляет узел односвязного списка:

Так как параметр T  неограничен, то компилятор заменяет его Object -ом:

В следующем примере обобщённый класс Node  использует ограниченный параметр:

Компилятор Java заменяет ограниченный параметр T  первой границей Comparable :

Стирание типа в обобщённых методах

Компилятор Java также стирает параметры типа обобщённых методов. Рассмотрите следующий обобщённый метод:

Так как T  неограничен, то компилятор Java заменяет его на Object :

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

Вы можете написать метод, рисующий разных монстров:

омпилятор Java заменит T  на Monster :

Влияние стирания типа и методы-мосты (bridge methods)

Иногда стирание типа приводит к ситуации, которую вы не ожидали. Следующие примеры показывают, как это может произойти.

Пусть есть два класса:

Рассмотрим следующий код:

После стирания типа код станет таким:

Что происходит при выполнении кода:

  • n.setData("Hello");  приводит к вызову метода setData(Object)  у объекта класса MyNode . (Класс MyNode  унаследовал setData(Object)  от Node ).
  • В теле метода setData(Object) , в поле data  объекта присваивается String.
  • К полю data  того же самого объекта можно обратиться через переменную mn , и будет ожидаться, что оно содержит Integer , так как mn  ссылается на экземпляр класса MyNode , который является Node<Integer> .
  • Попытка присвоить String  к Integer-у приводит к исключению ClassCastException в операции приведения типа, вставляемой компилятором Java.

Методы-мосты (Bridge Methods)

При компиляции класса или интерфейса, который расширяет параметризованный класс или реализует параметризованный интерфейс, компилятор требует создания синтетического метода, называемого методом-мостом (bridge method). Обычно вам не придётся беспокоиться об этих методах, но вы можете быть озадачены, если он появится в трассировке стека:

После стирания типа классы Node  и MyNode  станут такими:

После стирания типа сигнатуры методов не совпадают. Метод из класса Node  становится setData(Object) , а метод из класса MyNode  становится setData(Integer) , поэтому метод setData  из класса MyNode  не переопределяет метод setData  из класса Node .

Чтобы исправить проблему и сохранить полиморфизм обобщённых типов после стирания типа, компилятор Java генерирует методы-мосты, для того чтобы расширение работало как ожидается. Для класса MyNode  компилятор генерирует следующий метод-мост setData:

Как вы можете видеть, метод-мост, которые имеет ту же сигнатуру, что и метод setData  у класса Node  делегирует действие к оригинальному методу setData .
c
Я так и не смог нормально перевести слово “Non-Reifiable”, но пусть будет переводиться как «нематериализуемые».

В разделе стирания типов обсуждается процесс, где компилятор удаляет информацию, связанную с параметрами типа и аргументами типа. Стирание типа имеет последствия, связанные с произвольным количеством параметров (varargs).

Материализуемые типы (reifiable types) — это типы, информация о которых полностью доступа во время выполнения: примитивы, необобщённые типы, сырые типы, обращения к неограниченным подстановочным символам.

Нематериализуемые типы (Non-reifiable types) — это типы, информация о которых удаляется во время компиляции стиранием типов: обращения к обобщённым типам, которые не объявлены с помощью неограниченных подстановочных символов. Во время выполнения о нематериализуемых типах (Non-reifiable types) нет всей информации. Примеры нематериализуемых типов: List<String>  и List<Number> . Виртуальная машина Java не может узнать разницу между ними во время выполнения. В некоторых ситуациях нематериализуемые типы не могут использоваться, например в выражениях instanceof  или в качестве элементов массива.

Загрязнение кучи (Heap pollution)

Загрязнение кучи (heap pollution) возникает, когда переменная параметризованного типа ссылается на объект, который не является параметризованным типом. Такая ситуация возникает, если программа выполнила некоторую операцию, которая генерирует предупреждение unchecked warning во время компиляции. Предупреждение unchecked warning генерируется, если правильность операции, в которую вовлечён параметризованный тип (например приведение типа или вызов метода) не может быть проверена. Например, загрязнение кучи возникает при смешивании сырых типов и параметризованных типов, или при осуществлении непроверяемых преобразований типа.

В обычных ситуациях, когда код компилируется в одно и то же время, компилятор генерирует unchecked warning, чтобы привлечь ваше внимание к загрязнению кучи. Если вы компилируете различные части вашего кода отдельно, то становится трудно определить потенциальную угрозу загрязнения кучи. Если вы обеспечите компиляцию кода без предупреждений, то загрязнение кучи (heap pollution) не сможет произойти.

Потенциальные уязвимости методов с произвольным числом параметров с нематериализуемыми формальными параметрами

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

Рассмотрите следующий класс ArrayBuilder :

Следующий пример использует класс ArrayBuilder :

При компиляции возникает следующее предупреждение на определении метода ArrayBuilder.addToList :

Когда компилятор сталкивается с методом с произвольным числом параметров (varargs-метод), он преобразует varargs-параметр в массив. Однако Java не запрещает создание массивов с параметризованными типами. В методе ArrayBuilder.addToList  компилятор преобразует формальный параметр T... elements  в формальный параметр T[] elements . Но из-за стирания типа компилятор конвертирует формальный параметр в Object[] elements , поэтому возникает загрязнение кучи.

Следующая инструкция присваивает varargs-параметр l  в массив объектов objectArgs :

Эта инструкция потенциально приводит к загрязнению кучи. Значение, которое не соответствует параметризованному типу varargs-параметра l  может быть присвоено переменной в objectArray  и таким образом может быть присвоено в l . Но компилятор не генерирует unchecked warning в этой инструкции, так как он уже сгенерировал предупреждение, когда преобразовывал varargs-параметр List<String>... l  в формальный параметр List[] l . Инструкция корректна, переменная l  имеет тип List[] , который является дочерним типом для Object[] .

В результате компилятор не сгенерирует никаких ошибок или предупреждений, если вы присвоите объект List  объектов любого типа к элементу массива ObjectArray , как показано здесь:

Эта инструкция присваивает первому элементу массива objectArray  список List , содержащий объекты типа Integer .

Предположим, что вы вызываете метод ArrayBuilder.faultyMethod  следующим образом:

Во время выполнения виртуальная машина Java генерирует ClassCastException  на следующей инструкции:

Объект, сохранённый в первом элементе массива переменной l , имеет тип List<Integer> , но инструкция ожидает объект типа List<String> .

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

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

Аннотация @SafeVarargs  относится к документируемой части объявления метода. Эта аннотация говорит, что эта реализация метода корректно обрабатывает varargs-параметр.

Также возможно, но менее желательно, использовать следующую аннотацию для подавления этих предупреждений:

Но этот способ не подавляет предупреждения со стороны вызова метода. Смотрите «Java 8 аннотации».

Ограничения обобщений

Нельзя создавать экземпляры обобщённых типов с примитивными типами в качестве аргументов типа.

Рассмотрите следующий класс:

При создании объекта Pair  вы не можете заменять примитивным типом формальные параметры K  и V :

Вы можете заменить их только непримитивными типами:

Компилятор использует автоупаковку Integer.valueOf(8)  и Character('a') :

Нельзя создавать экземпляры параметров типа

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

В качестве обходного пути вы можете создать объект параметра типа с помощью отражения (reflection):

Вы можете вызвать метод append вот так:

Нельзя объявлять статические поля с типом параметра типа

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

Если бы статические поля с типом параметра типа были бы разрешены, то следующий код сбивал бы с толку:

Так как статическое поле os  является общим для phone , pager  и pc , то какого типа os? Оно не может быть Smartphone , Pager  и TabletPC  в одно и то же время, поэтому вы не можете создавать статические поля с типом параметра типа.

Нельзя использовать приведения типа или instanceof с параметризованными типами

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

Множество параметризованных типов, которые можно передать в метод rtti :

Во время выполнения нет параметров типа, поэтому мы не можем различить ArrayList<Integer>  и ArrayList<String>. Наибольшее, что вы можете сделать — это использовать подстановочный символ для проверки, что список является типом ArrayList :

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

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

Невозможно создавать массивы параметризованных типов

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

Следующий код показывает, что случится при вставке различных типов в массив:

Если вы попробуете то же самое с обобщённым списком, то будет такая проблема:

Если бы массивы с параметризованными типами были бы разрешены, то предыдущий код не смог бы бросить исключение ArrayStoreException.

Нельзя создавать, ловить (catch) или бросать (throw) объекты параметризованных типов

Обобщённый класс не может расширять класс Throwable  напрямую или ненапрямую. Например. следующие классы не компилируются:

Метод не может ловить (catch) экземпляр параметра типа:

Однако вы можете использовать параметр типа в клаузе throws :

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

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

Этот код не будет компилироваться.

Цикл статей «Учебник Java 8».

Следующая статья — «Java 8 исключения».
Предыдущая статья — «Java 8 ещё раз о перегрузке методов».

Java 8 обобщения: 7 комментариев

  1. Я может ошибаюсь, но в теме «Подстановочные символы и дочерние типы» содержится ошибка.
    В диаграмме нарисовано, что как будто бы класс Number дочерний от класса Integer:
    | List |
    ^
    |
    | List |

      1. Но смысл я понял. В диаграмме действительно была ошибка. В пункте «Подстановочные символы и дочерние типы».

        Связь есть между List<? extends Integer> и List<? extends Number>.

        А также есть связь между List<? super Number> и List<? super Integer>.

        Мы можем присвоить переменной типа List<? extends Number> значение типа List<? extends Integer>, но не наоборот.

        И мы можем присвоить переменной типа List<? super Integer> значение List<? super Number>, но не наоборот.

        Здесь я имею в виду присваивание без явного приведения типа, разумеется. С явным приведением типа можно присвоить и туда и сюда, и это даже будет работать.

        Сейчас диаграмму исправил, спасибо.

          1. Да, со схемой я намудрил что-то. Тут тема такая, воображение нужно сильное. Сейчас исправил. Теперь, надеюсь, правильно:)

  2. В «Обобщённые методы», в примере, в третьей строке
    static void setIfNull(Lair lair, T t) {
    «void» — лишнее слово, опечатка?
    — это и есть возвращаемый тип, так ведь?
    Или я что-то не правильно понял?

    1. В том примере void — это что нет результата метода, а то, что перед этим — это для указания, что метод обобщённый.

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

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