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 Голосовать
Обобщение типа данных, generic
Начиная с Java 5 появились новые возможности для программирования, к которым следует отнести поддержку обобщенного программирования, названная в Java generic. Эта возможность позволяет создавать более статически типизированный код. Соответственно, программы становятся более надежными и проще в отладке.
generic являются аналогией с конструкцией «Шаблонов»(template) в С++. Ожидалось, что дженерики Java будут похожи на шаблоны C++. На деле оказалось, что различия между generic’ами Java и шаблонами С++ довольно велики. В основном generic в Java получился проще, чем их C++-аналог, однако он не является упрощенной версией шаблонов C++ и имеют ряд значительных отличий. Так, в языке появилось несколько новых концепций, касающихся generic’ов – это маски и ограничения.
Рассмотрим 2 примера без использования и с использованием generic. Пример без использования generic с приведением типа (java casting):
В данном примере программист знает тип данных, размещамый в List’e. Тем не менее, необходимо обратить особое внимание на приведение типа («java casting»). Компилятор может лишь гарантировать, что метод next() вернёт Object, но чтобы обеспечить присвоение переменной типа Integer правильным и безопасным, требуется java casting. Приведение типа не исключает возможности появления ошибки «Runtime Error» из-за невнимательности разработчика.
Возникает вопрос: «Как с этим бороться? Каким образом зарезервировать List для определенного типа данных?». Данную проблему решают дженерики generic. В следующем примере используется generic без приведения типов.
В примере вместо приведения к Integer, был определен тип списка List. В этом заключается существенное отличие, и компилятор может проверить данный тип на корректность во время компиляции во всем коде. Эффект от generic особенно проявляется в крупных проектах: он улучшает читаемость и надежность кода в целом.
Свойства Generics
Объявление generic-класса
Объявить generic-класс совсем несложно. Пример такого объявления :
Пример использования generic-класса GenericSample :
Проблемы реализации generic
1. Wildcard
Рассмотрим процедуру dump, которой в качестве параметров передается Collection для вывода значений в консоль.
Проблема состоит в том что данная реализация кода не эффективна, так как Collection не является полностью родительской коллекцией всех остальных коллекций, грубо говоря Collection имеет ограничения. Для решения этой проблемы используется Wildcard («?»), который не имеет ограничения в использовании, то есть имеет соответствие с любым типом, и в этом его плюсы. И теперь, мы можем вызвать это с любым типом коллекции.
2. Bounded Wildcard
Рассмотрим процедуру draw, которая рисует фигуры, наследующие свойства родителя Shape. Допустим у Shape есть наследник Circle, и его необходимо «изобразить».
Использование позволяет использовать тип Cycle и всех его предков вполоть до Object.
3. Generic метод
Определим процедуру addAll, которая в качестве параметров получает массив данных Object[] и переносит его в коллекцию Collection
Ошибки, возникающие в последних строках связаны с тем, что нельзя просто вставить Object в коллекции неизвестного типа. Способ решения этой проблемы является использование «generic метода«. Для этого перед методом нужно объявить и использовать его.
Но все равно после выполнение останется ошибка в третьей строчке :
Допустим имеется функция, которая находит ближайший объект к точке Glyph из заданной коллекции. Glyph – это базовый тип, и может иметься неограниченное количество потомков этого типа. Также может иметься неограниченное количество коллекций, хранящих элементы, тип которых соответствует одному из этих потомков. Хотелось бы, чтобы функция могла работать со всеми подобными коллекциями, и возвращала элемент, тип которого совпадал бы с типом элемента коллекции, а не приводился к Glyph. Следующий пример не очень удачный:
Функция выглядит неплохо, но, тем не менее, не лишена недостатков. Получается так, что функции можно передать коллекцию любого типа. Это усложняет реализацию функции, порождая необходимость проверки типа элемента. Будет гораздо лучше написать так:
Теперь все встает на свои места, и в функцию можно передать только коллекцию, элементы которой реализуют интерфейс Glyph. generic сделал свое дело, код получился более типобезопасным.
4. Generic-классы
Наследование можно применять и для параметров generic-классов:
Как в методах, так и в классах можно задать более одного базового интерфейса, который должен реализовывать generic-параметр. Это делается при помощи следующего синтаксиса:
В данном примере generic-параметр должен реализовывать не только интерфейс Glyph, но и MoveableGlyph. Ограничений на количество интерфейсов, которые должен реализовывать переданный тип, нет. Но в класс можно передать только один, т.к. в Java нет множественного наследования. Типы в этом списке могут быть generic-типами, но ни один конкретный интерфейс не может появляться в списке более одного раза, даже с разными параметрами:
5. Bounded type argument
Метод копирования из одной коллекции в другую
6. Lower bounded wildcard
Метод нахождения максимума в коллекции
6. Wildcard Capture
Реализация метода Swap в List
Ограничения generic
Невозможно создать массив generic’ов :
Невозможно создать массив generic-классов :
Преобразование типов
В Generics также можно манипулировать с информацией, хранящийся в переменных.
Наследование исключений в generic’ах
Возможность использовать параметр generic-класса или метода в throws позволяет при описании абстрактного метода не ограничивать разработчика, использующего класс или интерфейс, конкретным типом исключения. Но использовать тип, заданный в качестве параметра, в catch-выражениях нельзя.
Необходимо добавить, что тип, переданный в качестве параметра, должен обязательно быть наследником Throwable.
Таким образом, generic-и в Java получились проще и внесли несколько интересных концепций, таких как маски (wildcard) и ограничения, которые, добавили удобство при работе и помогли решить проблемы. Но, как и любое усложнение языка, эти нововведения затрудняют его понимание и изучение. Появление generic-ов сделало язык 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 однажды, вы уже просто не сможете не пользоваться ей в дальнейшем.
— Ну, и напоследок еще одна маленькая лекция по Generic.
Сейчас я тебе расскажу, как обходить «стирание типов» (Type erasure).
— Ага. Мне тоже хочется это знать.
— Как ты уже наверное знаешь, в Java есть тип Class, который используется, чтобы хранить ссылку на объект класса. Примеры:
Но вот чего ты, наверное, не знаешь, так это того, что есть еще один класс Class, который является Generic’ом. И переменные этого Generic Class’а могут хранить только ссылки на тип, который был типом-параметром. Примеры:
— А почему оно так работает?
Но давай пойдем дальше.
Так вот, пользуясь тем фактом, что Class — это Generic, и тем, что переменная его типа может хранить значение только типа T, можно сделать вот такую хитрую комбинацию:
— Ага. Понимаю. Ничего сверхъестественного не произошло и страшного тоже. Ссылка на тип есть, пользоваться ей можно, работает и ладно.
— Вот, слышу слова «не мальчика, но мужа!» Работает и ладно – это часто самый оптимальный вариант.
Очень много всего можно было бы теперь переделать в Java, но нужно сохранять совместимость со старым кодом.
Именно десятки тысяч популярных отлаженных библиотек – это сегодня самый весомый аргумент в пользу Java. Так что, сохраняя обратную совместимость, Java остается самым популярным языком, и поэтому не может внедрять радикальные новшества.
— А я сделаю свою Java с блекджеком и …
— Ладно, я уже подустал за день. Давай до свидания.
— До свидания, Риша, и спасибо за такой интересный урок.
Практика применения Wildcards в Java: от простых Generic типов до подстановочных символов
Подстановочные символы Wildcards сегодня используются в большей степени для разработки библиотек и иногда в создании бизнес-приложений. Действительно мощный инструмент зачастую вызывает затруднение даже у senior программистов. Эксперт в области тестирования ПО, тренер Luxoft Training Денис Цыганов рассказал, в чем суть использования Wildcards и Generic в Java.
В чем суть
После появления Collection API в близком к современному виде разработчики Sun Microsystems (в дальнейшем и Oracle) искали решение для упрощения проверки безопасности по типам (typesafe) и ситуаций «загрязнения кучи» (heap pollution). Так появились Generic (обобщения), сделавшие возможным обнаружение несоответствия типов на этапе компиляции. До реализации этого механизма разработчики «ловили» указанные ошибки при непосредственном запуске приложения. В итоге, проблемы иногда могли впервые проявляться спустя годы использования продукта.
Разработчики Java внедряли Generics в существующую экосистему. Это заставило Oracle обеспечить обратную совместимость. Был выбран такой вариант реализации «дженериков», при котором все проверки выполнялись на этапе компиляции, а после происходило стирание типов (type erasure). То есть исполняемый код (скомпилированный с type erasure) ничем не отличается от скомпилированного кода без Generics. Следовательно, существующий не требовал изменения или перекомпиляции.
Как следствие такого решения, generic типы — инвариантны. Даже если generic типы находятся в отношении наследования, производные от этих типов в отношении наследования не находятся.
К примеру, следующая конструкция вызовет ошибку компиляции:
ArrayList list = new ArrayList ();
Это не всегда удобно, так как разработчикам часто хочется иметь метод с параметром, содержащим generic тип, который является более общим по отношения к используемым generic типам. Ожидая, что метод “охватит” эти типы. Например:
void doSomethingWithList(List list)
На первый взгляд, логично, что такой метод мог бы сортировать любые списки содержащие любых потомков Numbers, типа
Для начала попробуем обойтись без Wildcards. В решении данного вопроса разработчику не поможет стандартный для подобных случаев механизм перегрузки методов. Следуя этому подходу можно написать несколько методов:
void doSomethingWithList(List list)
void doSomethingWithList(List list)
void doSomethingWithList(List list)
К сожалению, такой код не скомпилируется благодаря тому же стиранию типов. Т.к. после все три метода имеют одинаковую сигнатуру:
void doSomethingWithList(List list)
Мы можем поиграть с названиями методов, но их тело, скорее всего, будет выглядеть абсолютно одинаково (или очень похоже), а это нарушает один из главных принципов разработки — DRY (Don’t Repeat Yourself).
Эту проблему можно решить с помощью Wildcards. Символ «?» может быть использован вместо Generic типа, обозначая любой тип. Также можно задать границы для семейства типов, определенных обобщенным классом, делая api понятным и изящным. Wildcards способен ограничивать тип вниз (extends) или вверх (super).
Важно понимать, что Wildcard тип мо жет иметь только ссылка, но не сам объект. То есть он может быть задан для локальной переменной, поля класса, параметра метода или типа возвращаемого значения, но не может быть использован в операторе new :
new ArrayList (); // не скомпилируется
Примеры объявления переменных c Wildcards:
Первая и вторая ссылки — ковариантны. Третья — контравариантна.
Что это означает?
Примечание 1: обозначенные ограничения относятся только к Generic типу. Так, например, следующий код скомпилируется без проблем:
List list = new ArrayList ();
Для краткости можно опускать generic тип при создании объекта:
List list = new ArrayList<>();
Эта возможность появилась в версии 1.7 и называется Diamonds (угловые скобки напоминают бриллиант).
Примечание 2: Массивы в отличии от generics ковариантны. Иными словами, если Объекты находятся в отношении наследования, то и их производные (массивы), находятся в том же отношении. Т.е. следующий код корректен:
Object[] array = new Integer[5];
Практика применения
Чтобы понять суть Generic разберем пример программирования до и после их появления. Мир без «генериков» выглядит так:
// Список объектов без ограничения по типу
//теперь он называется raw type List
List listOfStrings = new ArrayList();
//В него можно добавить элементы любого объектного типа
Для извлечения элемента нужно знать точный тип (для применения оператора Cast).
String string = (String) listOfStrings.get(0);
//есть шанс получить ClassCastException, если мы не угадали с типом
String secondString = (String) listOfStrings.get(1);
Эта ошибка проявится в среде выполнения (Runtime) только во время выполнения конкретной строки кода (если соответствующая ветка исполняется не часто, велик шанс эту ошибку не обнаружить на этапе разработки).
С появлением «генериков» код выглядит иначе. Ошибки проявляются намного раньше — во время разработки, а стоимость их исправления значительно ниже. Кроме того, при извлечении элемента из списка нет необходимости приводить тип, а также знать (читай — искать по коду) точный тип — он будет предложен IDE или упомянут компилятором.
Список элементов определенного типа — String в примере ниже.
List trueListOfStrings = new ArrayList<>();
//it accepts String parameter only
В случае использования несовместимого типа — получим ошибку компиляции.
String alwaysCorrectType = trueListOfStrings.get(0);
//Возможность получить ClassCastException пропадает вместе с Cast операцией.
Теперь рассмотрим задачу отбора животных из существующего списка по определенному условию (conditional copy). В этом примере мы уже будем применять Wildcards.
Заранее обозначим требования к нашему API:
Для начала определим типы животных:
boolean result = false;
//имитация некоторой логики
class Cat extends Animal <
//переопределяем логику isNeeded() для класса Cat
Подготовим несколько списков для тестирования:
List objects = new ArrayList<>();
List cats = new ArrayList<>();
Теперь попробуем решить задачу «в лоб»:
void conditionalCopy(List dst, List src) <
/* при такой сигнатуре метода нет возможности вызвать методы класса Animal, т.к. тип T в рамках метода не ограничен и может представлять любой тип */
Итак, у нас проблема с реализацией требования 3.
void conditionalCopy(List dst, List src) <
List list = src.stream()
Проблема решена, пробуем копировать:
conditionalCopy(animals, cats); //1
conditionalCopy(animals, animals); //2
conditionalCopy(objects, cats); //3
Строки 1 и 3 вызывают проблему компиляции. Мы помним, что «дженерики» и поэтому требование 4 не реализовано. Давайте решать эту проблему:
void conditionalCopy(List dst, List src) <
List list = src.stream()
conditionalCopy(animals, cats); //1
conditionalCopy(animals, animals); //2
conditionalCopy(objects, cats); //3
Самое время применить контравариантность:
void conditionalCopy(List dst, List src) <
List list = src.stream()
Последняя проверка подтверждает правильность решения. Все строки компилируются и отлично работают.
Теперь реализованы все требования.
Кстати, наш API не позволяет некорректные варианты копирования вроде таких:
conditionalCopy(cats, animals); //4
conditionalCopy(cats, objects); //5
Обе строки (4) и (5) вызывают ошибку компиляции.
Несмотря на кажущуюся простоту Wildcards стали камнем преткновения для многих программистов. Я думаю, что использование символа «?» в каком-то смысле запутывает разработчика, создавая ощущение, что объект готов работать с любым типом в любой момент. Однако это не так. «Любой» — это не «каждый» и, тем более, не «все сразу». В данном контексте правильно было бы сказать «какой-то конкретный, из допустимого множества типов». Для их правильного определения необходима практика. Однако для полного понимания разработчикам необходимо узнать основные механизмы стирания типов, правильно использовать ограничения extends и super, принцип PECS, raw type, проблему heap pollution, и механизм Generic в целом.