что такое wildcard и зачем он нужен java

Дженерики (Java, обучающая статья)

Предисловие

За основу данной статьи была взята информация из 6-ой главы книги «Oracle Certified Professional Java SE 7 Programmers Exams 1Z0-804 and 1Z0-805». Она была немного изменена (кое-где обрезана, а кое-где дополнена с помощью Google и Википедии). Здесь показаны далеко не все нюансы дженериков — для более подробной информации следует обратиться к официальной документации. Приятного прочтения.

Введение

Обобщённое программирование — это такой подход к описанию данных и алгоритмов, который позволяет их использовать с различными типами данных без изменения их описания. В Java, начиная с версии J2SE 5.0, добавлены средства обобщённого программирования, синтаксически основанные на C++. Ниже будут рассматриваться generics (дженерики) или > — подмножество обобщённого программирования.

Допустим мы ничего не знаем о дженериках и нам необходимо реализовать специфический вывод на консоль информации об объектах различного типа (с использованием фигурных скобок).

Ниже пример реализации:

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

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

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

Думаю, многие согласятся, что ошибка компиляции «лучше» ошибки времени выполнения, т.к. чисто теоретически скомпилированный код с ошибкой может попасть туда, куда ему лучше бы и не попадать. Это очевидное достоинство дженериков. Теперь подробнее рассмотрим конструкции, относящиеся к дженерикам в этом примере. Для того, чтобы код скомпилировался, достаточно заменить строку

Посмотрим на декларацию BoxPrinter:

После имени класса в угловых скобках » » указано имя типа «Т», которое может использоваться внутри класса. Фактически Т – это тип, который должен быть определён позже (при создании объекта класса).

Внутри класса первое использование T в объявлении поля:

Здесь объявляется переменная дженерик-типа (generic type), т.о. её тип будет указан позже, при создании объекта класса BoxPrinter.

В main()-методе происходит следующее объявление:

Здесь указывается, что Т имеет тип Integer. Грубо говоря, для объекта value1 все поля Т-типа его класса BoxPrinter становятся полями типа Integer (private Integer val;).
Ещё одно место, где используется T:

Как и в декларации val с типом Т, вы говорите, что аргумент для конструктора BoxPrinter имеет тип T. Позже в main()-методе, когда будет вызван конструктор в new, указывается, что Т имеет тип Integer:

Теперь, внутри конструктора BoxPrinter, arg и val должны быть одного типа, так как оба имеют тип T. Например следующее изменение конструктора:

приведёт к ошибке компиляции.

Последнее место использования Т в классе – метод getValue():

Тут вроде тоже всё ясно – этот метод для соответствующего объекта будет возвращать значение того типа, который будет задан при его (объекта) создании.

При создании дженерик-классов мы не ограничены одним лишь типом (Т) – их может быть несколько:

Нет ограничений и на количество переменных с использующих такой тип:

Алмазный синтаксис (Diamond syntax)

Вернёмся немного назад к примеру со строкой кода:

Если типы не будут совпадать:

То мы получим ошибку при компиляции:

Немного лениво каждый раз заполнять типы и при этом можно ошибиться. Чтобы упростить жизнь программистам в Java 7 был введён алмазный синтаксис (diamond syntax), в котором можно опустить параметры типа. Т.е. можно предоставить компилятору определение типов при создании объекта. Вид упрощённого объявления:

Следует обратить внимание, что возможны ошибки связанные с отсутствием «<>» при использовании алмазного синтаксиса

В случае с примером кода выше мы просто получим предупреждение от компилятора, Поскольку Pair является дженерик-типом и были забыты «<>» или явное задание параметров, компилятор рассматривает его в качестве простого типа (raw type) с Pair принимающим два параметра типа объекта. Хотя такое поведение не вызывает никаких проблем в данном сегменте кода, это может привести к ошибке. Здесь необходимо пояснение понятия простого типа.

Посмотрим на вот этот фрагмент кода:

Теперь посмотрим на вот этот:

По результатам выполнения оба фрагмента аналогичны, но у них разная идея. В первом случае мы имеем место с простым типом, во вторым – с дженериком. Теперь сломаем это дело – заменим в обоих случаях

Для простого типа получим ошибку времени выполнения (java.lang.ClassCastException), а для второго – ошибку компиляции. В общем, это очень похоже на 2 самых первых примера. Если в двух словах, то при использовании простых типов, вы теряете преимущество безопасности типов, предоставляемое дженериками.

Универсальные методы (Generic methods)

По аналогии с универсальными классами (дженерик-классами), можно создавать универсальные методы (дженерик-методы), то есть методы, которые принимают общие типы параметров. Универсальные методы не надо путать с методами в дженерик-классе. Универсальные методы удобны, когда одна и та же функциональность должна применяться к различным типам. (Например, есть многочисленные общие методы в классе java.util.Collections.)

Рассмотрим реализацию такого метода:

Нам в первую очередь интересно это:

» » размещено после ключевых слов «public» и «static», а затем следуют тип возвращаемого значения, имя метода и его параметры. Такое объявление отлично от объявления универсальных классов, где универсальный параметр указывается после имени класса. Тело метода вполне обычное – в цикле все элементы списка устанавливаются в одно значение (val). Ну и в main()-методе происходит вызов нашего универсального метода:

Стоит обратить внимание на то, что здесь не задан явно тип параметра. Для IntList – это Integer и 100 тоже упаковывается в Integer. Компилятор ставит в соответствие типу Т – Integer.

А сейчас вопрос – какая (-ие) из нижеприведённых строк откомпилируется без проблем?

Ответ с пояснением:
Первый вариант неправильный, т.к. нельзя создавать объект интерфейса.
Во втором случае мы создаем объект типа ArrayList и ссылку на него базового для ArrayList класса. И там, и там дженерик-тип одинаковый – всё правильно.
В третьем и четвёртом случае будет иметь ошибка компиляции, т.к. дженерик-типы должны быть одинаковыми (связи наследования здесь никак не учитываются).

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

Будем думать от обратного – допустим 3-ий вариант возможен. Рассмотрим такой код:

Wildcards (Маски)

Сейчас будут рассмотрены Wildcard Parameters (wildcards). Этот термин в разных источниках переводится по-разному: метасимвольные аргументы, подстановочные символы, групповые символы, шаблоны, маски и т.д. В данной статье я буду использовать «маску», просто потому, что в ней меньше букв…

Как было написано выше вот такая строка кода не скомпилируется:

Но есть возможность похожей реализации:

Под маской мы будем понимать вот эту штуку – » «.

А сейчас пример кода использующего маску и пригодного к компиляции:

Метод printList принимает список, для которого в сигнатуре использована маска:

И этот метод работает для списков с различными типами данных (в примере Integer и String).

Однако вот это не скомпилируется:

И ещё один маленький пример:

Тут не возникнет проблем компиляции. Однако нехорошо, что переменная numList хранит список со строками. Допустим нам нужно так объявить эту переменную, чтобы она хранила только списки чисел. Решение есть:

Данный код не скомпилируется, а всё из-за того, что с помощью маски мы задали ограничение. Переменная numList может хранить ссылку только на список, содержащий элементы унаследованные от Number, а всё из-за объявления: List numList. Тут мы видим, как маске задаётся ограничение – теперь numList предназначен для списка с ограниченным количеством типов. Double как и Integer наследуется от Number, поэтому код приведённый ниже скомпилируется.

То, что было описано выше называется ограниченными масками (Bounded wildcards). Применение таких конструкций может быть весьма красивым и полезным. Допустим нам необходимо посчитать сумму чисел различного типа, которые хранятся в одном списке:

Double-тип был использован для переменной result т.к. он без проблем взаимодействует с другими числовыми типами (т.е. не будет проблем с приведением типов).

На этом все. Надеюсь, данная статья была полезной.

Если Вам понравилась статья, проголосуйте за нее

Голосов: 175 Голосовать что такое wildcard и зачем он нужен java. Смотреть фото что такое wildcard и зачем он нужен java. Смотреть картинку что такое wildcard и зачем он нужен java. Картинка про что такое wildcard и зачем он нужен java. Фото что такое wildcard и зачем он нужен java

Источник

Использование generic wildcards для повышения удобства Java API

Доброго времени суток!

Исходный API

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

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

Однако, в чуть менее тривиальных случаях клиент вашего API столкнётся с неприятными ограничениями.

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

А ведь всё решается так просто! Нам нужно лишь слегка изменить сигнатуру метода:

Запись Predicate означает «предикат от V или любого супертипа V (вплоть до Object)». Данное изменение никак не ломает компиляцию существующего кода, зато устраняет абсолютно бессмысленные ограничения на параметр предиката. Клиент теперь может использовать свой предикат для Vehicle совершенно свободно:

Мы обобщим данный приём чуть ниже, и запомнить его будет совсем просто.

Запись Map буквально означает «мапка с ключами типа K или любого из подтипов K и со значениями типа V или любого из подтипов V».

Принцип PECS — Producer Extends Consumer Super

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

Этот принцип Joshua Bloch называет PECS (Producer Extends Consumer Super), а авторы книги Java Generics and Collections (Maurice Naftalin, Philip Wadler) — Get and Put Principle. Но давайте остановимся на PECS, запомнить проще. Этот принцип гласит:

Производитель и потребитель, кто это такие? Очень просто: если метод читает данные из аргумента, то этот аргумент — производитель, а если метод передаёт данные в аргумент, то аргумент является потребителем. Важно заметить, что определяя производителя или потребителя, мы рассматриваем только данные типа T.

В нашем примере Predicate — это потребитель (метод getAll(Predicate ) передаёт в этот аргумент данные типа T), а Map — производитель (метод putAll(Map ) читает данные типа T — в данном случае под T подразумевается K и V — из этого аргумента).

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

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

Вооружившись PECS-принципом, мы можем теперь пройтись по всем методам нашего MyObjectStore интерфейса и сделать улучшения там, где это требуется. Методы put(K, V) и get(K) улучшений не требуют (т.к. они не имеют аргументов с параметризованным типом); методы putAll(Map ) и getAll(Predicate ) мы уже и так улучшили, дальше некуда; а вот метод getAll(Collection ) имеет аргумент-производитель с параметризованным типом, который мы можем расширить. Вместо

и радуемся новому, более удобному API! (Заметьте, возвращаемое значение мы не трогаем!)

Другие примеры потребителя и производителя

Производителями могут быть не только коллекции. Самый очевидный пример производителя — это фабрика:

Хорошим примером аргумента, являющегося и производителем, и потребителем, будет аргумент вот такого типа:

Коллекция может быть потребителем в случае, если это ouput-коллекция, в которую метод складывает результат своей работы (хотя такой стиль в Java редко используется и считается плохим тоном).

Заключение

В этой статье мы познакомились с принципом PECS (Producer Extends Consumer Super) и научились его применять при разработке API на Java. Как показывает практика, даже в самых продвинутых программистских конторах об этом принципе некоторые разработчики не знают, и в результате проектируют не совсем удобное API. Но, к счастью, исправляются подобные ошибки очень легко, а запомнив мнемонику PECS однажды, вы уже просто не сможете не пользоваться ей в дальнейшем.

Источник

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

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

что такое wildcard и зачем он нужен java. Смотреть фото что такое wildcard и зачем он нужен java. Смотреть картинку что такое wildcard и зачем он нужен java. Картинка про что такое wildcard и зачем он нужен java. Фото что такое wildcard и зачем он нужен java

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

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

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

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

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

ТипПодтип
NumberInteger
ListArrayList
CollectionList
IterableCollection

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

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

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

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

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

Wildcards

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

Это ковариантность. List — подтип List

что такое wildcard и зачем он нужен java. Смотреть фото что такое wildcard и зачем он нужен java. Смотреть картинку что такое wildcard и зачем он нужен java. Картинка про что такое wildcard и зачем он нужен java. Фото что такое wildcard и зачем он нужен javaextends 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

На скриншоте ниже два примера программы:
что такое wildcard и зачем он нужен java. Смотреть фото что такое wildcard и зачем он нужен java. Смотреть картинку что такое wildcard и зачем он нужен java. Картинка про что такое wildcard и зачем он нужен java. Фото что такое wildcard и зачем он нужен java

Разница между ними в том, что слева происходит 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 подходит к концу. Вот другие источники, которые помогут вам в освоении темы:

Источник

Автор: Брайен Гетц (brian.goetz@sun.com)
главный инженер, Sun Microsystems
Перевод: М. Орельская
Источник: http://www.ibm.com/developerworks

Опубликовано: 28.04.2009

Понимание wildcard capture

Одним из наиболее сложных аспектов generic-типов (обобщенных типов) в языке Java являются wildcards (подстановочные символы, в данном случае – «?»), и особенно – толкование и разбор запутанных сообщений об ошибках, происходящих при wildcard capture (подстановке вычисляемого компилятором типа вместо wildcard). В данной части «Теория и практика Java» (Java theory and practice) старейший Java-разработчик Брайен Гетц расшифровывает некоторые из наиболее загадочно выглядящих сообщений об ошибках, выдаваемых компилятором «javac», и предлагает решения и варианты обхода, которые помогут упростить использование generic-типов.

Дискуссия по поводу generic-типов не прекращается с тех самых пор, как они были добавлены в язык Java в JDK 5. Одни считают, что эти типы упрощают программирование за счет расширения системы типов и, следовательно, повышают способность компилятора проверять безопасность типов. Другие говорят, что введение generic-типов только увеличивает путаницу. Все мы при их использовании сталкивались с некоторыми моментами, заставляющими задуматься, но именно wildcards представляют собой самую сложную часть generic-типов.

Основы работы с wildcards

Generic-и являются средством выражения ограничений поведения классов или методов для исходно неизвестных типов. Например: «какой бы из типов ни использовался в этом методе для параметров x или y, эти параметры должны быть одинакового типа», «вы должны обеспечить параметр одинакового типа для обоих этих методов», или «возвращаемое значение foo() имеет тот же тип, что и параметр bar()».

Wildcards – специальные вопросительные знаки, отмечающие место, где должно оказаться название параметра типа. Они являются средством выражения ограничений в условиях неизвестности какого-нибудь типа. Первоначально эти знаки не являлись частью структуры generic-ов, унаследованной от проекта Generic Java (GJ), а были добавлены в процессе разработки дизайна за те более чем пять лет, что прошли между созданием JSR 14 и его последним выпуском.

Wildcards играют важную роль в системе типов. Они позволяют задать границы для семейства типов, определенных каким-нибудь generic-классом. Для generic-класса ArrayList тип ArrayList является супер-типом ArrayList с параметром любого типа T (так же, как обычный тип (raw type) ArrayList и корневой тип (root type) Object, но эти супертипы менее удобны при выполнении вывода типа).

У меня появился wildcard. Что теперь?

Листинг 1 содержит пример простого типа-контейнера Box, который поддерживает операции put и get. Box параметризован параметром T, описывающим тип содержимого контейнера. Например, Box может включать в себя только элементы типа String.

Листинг 1. Простой generic-тип Box

Одно из преимуществ wildcards состоит в том, что они дают возможность написать код, который может оперировать переменными generic-типов без знания их точных границ. Допустим, у вас есть переменная типа Box – такая, как параметр box в методе unbox() в листинге 2. Что же может делать unbox(), обрабатывая параметр box?

Листинг 2. Метод Unbox с wildcard-параметром

Что знает unbox() о типе возвращаемого значения у метода box.get()? Он знает, что это тип T для какого-то неизвестного T. Поэтому лучшее, что он может сделать – принять решение, согласно которому тип возвращаемого значения от get() является замещением неизвестного ранее типа T. В случае неограниченного wildcard этот тип представлен типом Object. Таким образом, выражение box.get() в листинге 2 возвращает значение типа Object.

Wildcard capture

Листинг 3 содержит код, который похож на работающий, но только похож. В данном случае берется экземпляр generic-типа Box, из него получается значение, и производится попытка установить это значение обратно в тот же Box.

Листинг 3. Взятое из box значение не может быть помещено обратно


Capture helpers

Несмотря на то, что компилятор отбрасывает здесь какую-то полезную информацию, существует способ заставить его данную информацию восстановить. Для этого нужно дать неизвестному wildcard-типу имя. В Листинге 4 показана реализация rebox(), включающая вспомогательный generic-метод, который это и делает:

Листинг 4. Использование хелпера

Вспомогательный метод reboxHelper() является generic-методом. Generic-методы вводят дополнительные параметры типов (помещаемые в угловые скобки перед типом возвращаемого значения), которые обычно используются для формулирования ограничений типов между параметрами и/или возвращаемым значением метода. Однако в случае reboxHelper() generic-метод не задействует параметр типа для определения ограничения типа, а позволяет компилятору – через вывод типа – дать имя параметру типа переменной box.

Можно было объявить и rebox() как generic-метод, как мы объявили reboxHelper(), но в разработке API это не считается хорошим стилем. Основной принцип разработки здесь – не давать никакого имени, если на это имя никогда не будет ссылок. В контексте generic-методов это означает, что если параметр типа показывается в сигнатуре метода только один раз, то он, скорее, должен быть wildcard, нежели именованным параметром типа. Вообще же, API с wildcards проще, чем API с generic-методами; разрастание имен типов в более сложном объявлении методов может сделать это объявление менее читаемым. Поскольку всегда существует возможность при необходимости восстановить имя с помощью private capture-хелпера, вышеописанный подход дает способ сохранять API чистыми, не удаляя при этом нужную информацию.

Вывод типа

Прием с capture-хелпером основан на выводе типов (type inference) и преобразовании при фиксации (capture conversion). Java-компилятор не выполняет вывода типов во многих местах, однако для параметров типа в generic-методах он это делает (другие языки гораздо больше полагаются на вывод типов, поэтому в будущем, возможно, разработчики расширят функции вывода типов в Java). При необходимости можно указать значение параметра типа, но только в том случае, если существует возможность именовать этот тип – а capture-типы нельзя именовать. Поэтому данный прием сработает лишь в том случае, если компилятор выводит тип сам. Именно преобразование позволяет компилятору создать заглушку названия типа для wildcard так, чтобы при выводе типов был выведен именно этот тип.

Компилятор будет стараться вывести наиболее подходящий тип для имеющихся параметров типа при разрешении вызова generic-метода. К примеру, для такого generic-метода:

компилятор мог бы решить, что T имеет тип Integer, Number, Serializable или Object, но выбирает Integer, поскольку он является наиболее точно удовлетворяющим ограничениям.

Такое нарушение «принципа DRY» (Don’t Repeat Yourself – «Не повторяйся!») может раздражать, даже когда IDE готовы проделать за вас некоторую часть работы. Однако если реализация класса BoxImpl поддерживает generic-метод фабрики класса, как показано в Листинге 5 (это, в любом случае, хорошая идея), есть возможность сократить подобную избыточность в клиентском коде:

Листинг 5. Generic-метод фабрики класса, позволяющий избежать чрезмерных описаний в параметрах типа.

Если Box создается с использованием фабрики BoxImpl.make(), параметр типа достаточно указать всего один раз:

Помимо уменьшения количества нажатий на клавиши, продемонстрированная здесь техника использования фабричного метода имеет и другие преимущества по сравнению с использованием конструкторов: фабрикам классов можно давать более подробные имена, фабрики классов способны возвращать подтипы именованных типов возвращаемых значений, и они не обязательно должны создавать новый экземпляр объекта при каждом вызове, поскольку позволяют совместное использование неизменяемых экземпляров. (Чтобы узнать больше о преимуществах статических фабрик классов, обратитесь к статье «Effective Java, Item #1», см. раздел «Ресурсы»).

Заключение

Несомненно, wildcards могут вызывать затруднения. Одни из самых запутанных сообщений об ошибках, выдаваемых компилятором Java, связаны как раз с применением wildcards. Некоторые из наиболее сложных разделов спецификации языка Java также посвящены wildcards. Тем не менее, при правильном использовании wildcards чрезвычайно удобны. В данной статье описано два приема – использование capture-хелперов и generic-фабрик, преимущество которых заключается в использовании generic-методов и вывода типов. При надлежащем применении они помогают устранить значительную часть сложностей.

Ресурсы

Copyright © 1994-2016 ООО «К-Пресс»

Источник

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

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