для чего нужны дженерики 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 Голосовать
Дженерики в Java для самых маленьких: синтаксис, границы и дикие карты
Разбираемся, зачем нужны дженерики и как добавить их в свой код.
Оля Ежак для Skillbox Media
У нас в парадной подъезде рядом с почтовыми ящиками стоит коробка. Предполагалось, что туда будут выбрасывать бумажный спам, который какие-то вредители упорно кладут в эти самые ящики. Но в коробке вместе с бумажками лежат пустые бутылки и банки, подозрительного вида пакеты, а в нынешних реалиях — ещё и использованные медицинские маски. Почему люди так делают? Потому что так можно.
Теперь представьте, что содержимое коробки вы отвозите на переработку, а перед этим каждый раз приходится отделять бумагу от прочего мусора. Не хотели бы вы заполучить такую коробку, которая не даст положить в себя что-то, кроме бумаги? Если ваш ответ «да» — вам понравятся дженерики (generics).
Содержание
Фулстек-разработчик. Любимый стек: Java + Angular, но в хорошей компании готова писать хоть на языке Ада.
Знакомимся с дженериками
До появления дженериков программисты могли неявно предполагать, что какой-то класс, интерфейс или метод работает с элементами определённого типа.
Посмотрите на этот фрагмент кода:
Здесь предполагается, что метод printSomething работает со списком строк. Мы можем догадаться об этом, потому что в цикле все элементы приводятся (преобразуются) к классу String, а потом ещё и метод length этого класса вызывается.
Но смотрите, что сделали программисты Саша и Маша, — они поленились заглянуть внутрь метода и положили в список: один — число, а вторая — экземпляр StringBuilder.
Вот только тестировщик назначил баг не кому-то из них, а Паше, который написал метод printSomething, — потому что ошибка произошла именно во время его выполнения.
Паша быстро нашёл истинных виновников и попросил их исправить заполнение списка. Но на будущее решил подстраховаться от подобных ситуаций и переписал метод с использованием дженериков. Вот так:
Теперь, если кто-то захочет положить в массив нестроковый элемент, ошибка станет заметной сразу — ещё на этапе компиляции.
Обратите внимание, что во второй версии Пашиного метода item не приводится насильно к типу String. Мы просто получаем в цикле очередной элемент списка, и компилятор соглашается, что это, очевидно, будет строка. Код стал менее громоздким, читать его стало проще.
Объявляем дженерик-классы и создаём их экземпляры
Давайте запрограммируем ту самую коробку, о которой шла речь в начале статьи: создадим класс Box, который умеет работать только с элементами определённого типа. Пусть для простоты в этой коробке пока будет только один элемент:
В классе два метода:
Во всех случаях, кроме заголовка класса, символ T пишется без угловых скобок, он обозначает один и тот же параметр типа.
Теперь создадим коробку для бумаги. Пусть за бумагу отвечает класс Paper, а значит, экземпляр правильной коробки создаётся вот так:
Это полный вариант записи, но можно и короче:
Так как слева мы уже показали компилятору, что нужна коробка именно для бумаги, справа можно опустить повторное упоминание Paper — компилятор «догадается» о нём сам.
Это «угадывание» называется type inference — выведение типа, а оператор « <>» — это diamond operator. Его так назвали из-за внешнего сходства с бриллиантом.
E — element, для элементов параметризованных коллекций;
K — key, для ключей map-структур;
V — value, для значений map-структур;
N — number, для чисел;
T — type, для обозначения типа параметра в произвольных классах;
S, U, V и так далее — применяются, когда в дженерик-классе несколько параметров.
Дженерик-классы хороши своей универсальностью: с классом Box теперь можно создать не только коробку для бумаги, но и, например, коробки для сбора пластика или стекла:
А можно пойти ещё дальше и создать дженерик-класс с двумя параметрами для коробки с двумя отсеками. Вот так:
Теперь легко запрограммировать коробку, в одном отсеке которой будет собираться пластик, а во втором — стекло:
Обратите внимание, что type inference и diamond operator позволяют нам опустить оба параметра в правой части.
Объявляем и реализуем дженерик-интерфейсы
Объявление дженерик- интерфейсов похоже на объявление дженерик-классов. Продолжим тему переработки и создадим интерфейс пункта переработки GarbageHandler сразу с двумя параметрами: тип мусора и способ переработки:
Реализовать (имплементить) этот интерфейс можно в обычном, не дженерик- классе:
Но можно пойти другим путём и сначала объявить дженерик-класс с двумя параметрами:
Или скомбинировать эти два способа и написать дженерик-класс только с одним параметром:
Дженерик-классы и дженерик-интерфейсы вместе называются дженерик-типами.
Можно создавать экземпляры дженерик-типов «без расшифровки», то есть никто не запретит вам объявить переменную типа Box — просто Box:
Для такого случая даже есть термин — raw type, то есть «сырой тип». Эту возможность оставили в языке для совместимости со старым кодом, который был написан до появления дженериков.
В новых программах так писать не рекомендуется. Да и зачем? Ведь при таком способе теряются все преимущества использования дженериков.
Пишем дженерик-методы
В примерах выше мы уже видели параметризованные методы в дженерик-классах и интерфейсах. Типизированными могут быть как параметры метода, так и возвращаемый тип.
До этого мы использовали в методах только те обозначения типов, которые объявлены в заголовке дженерик-класса или интерфейса, но это не обязательно. Предположим, у нашего пункта переработки есть ещё опция: сбор опасных отходов, которые сотрудники вывозят на утилизацию в другое место. Напишем метод для этого:
У метода transfer есть свой личный параметр для типа, который не обязан совпадать ни с типом T, ни с типом S. При первом упоминании новый параметр, как и в случае с заголовком класса или интерфейса, пишется в угловых скобках.
Дженерик-методы можно объявлять и в обычных (не дженерик) классах и интерфейсах. Наш класс для переработки мог быть выглядеть так:
Здесь дженерики используются только в методе.
Обратите внимание на синтаксис: параметры типов объявляются после модификатора доступа ( public), но перед возвращаемым типом ( void). Они перечисляются через запятую в общих угловых скобках.
Ограничиваем дженерики сверху и снизу
Давайте немного расширим наше представление о мусоре и введём для него дополнительное свойство — массу «типичного представителя», то есть массу одной пластиковой бутылки или листка бумаги, например.
Теперь попробуем использовать эту массу в методе уже знакомого класса Box:
И получим ошибку при компиляции: мы не рассказали компилятору, что T — это какой-то вид мусора. Исправим это с помощью так называемого upper bounding — ограничения сверху:
Теперь метод getItemWeight успешно скомпилируется.
Здесь T extends Garbage означает, что в качестве T можно подставить Garbage или любой класс-наследник Garbage. Из уже известных нам классов это могут быть, например, Paper или Plastic. Так как и у Garbage, и у всех его наследников есть метод getWeight, его можно вызывать в новой версии дженерик-класса Box.
Для одного класса или интерфейса можно добавить сразу несколько ограничений. Вспомним про интерфейс для пункта приёма мусора и введём класс для метода переработки — HandleMethod. Тогда GarbageHandler можно переписать так:
Wildcards
Термин wildcard пришёл в программирование из карточной игры. В покере, например, так называют карту, которая может сыграть вместо любой другой. Джокер — известный пример такой «дикой карты».
Wildcard нельзя подставлять везде, где до этого мы писали буквенные обозначения. Не получится, например, объявить класс Box или дженерик-метод, который принимает такой тип:
Wildcards удобно использовать для объявления переменных и параметров методов совместно с классами из Java Collection Framework — здесь собраны инструменты Java для работы с коллекциями. Если вы не очень хорошо знакомы с ними, освежите знания, прочитав эту статью.
В примере ниже мы можем подставить вместо «?» любой тип, в том числе Paper, поэтому строка успешно скомпилируется:
Wildcards можно применять для ограничений типов:
Это уже знакомое нам ограничение сверху, upper bounding, — вместо «?» допуст им Garbage или любой его класс-наследник, то есть Paper подходит.
Но можно ограничить тип и снизу. Это называется lower bounding и выглядит так:
Собираем понятия, связанные с дженериками
Мы не успели разобраться с более сложными вещами — например, с заменами аргументов типов в классах-наследниках, с переопределением дженерик-методов, не узнали об особенностях коллекций с wildcards.
Обо всём этом и не только — в следующих статьях. А пока соберём небольшой словарик из терминов, которые связаны с использованием дженериков, — они пригодятся при чтении специальной литературы:
Термин | Расшифровка |
---|---|
Дженерик-типы (generic types) | Дженерик-класс или дженерик-интерфейс с одним или несколькими параметрами в заголовке |
Параметризованный тип (parameterized types) | Вызов дженерик-типа. Для дженерик-типа List параметризованным типом будет, например, List |
Параметр типа (type parameter) | Используются при объявлении дженерик-типов. Для Box T — это параметр типа |
Аргумент типа (type argument) | Тип объекта, который может использоваться вместо параметра типа. Например, для Box Paper — это аргумент типа |
Wildcard | Обозначается символом «?» — неизвестный тип |
Ограниченный wildcard (bounded wildcard) | Wildcard, который ограничен сверху — или снизу — |
Сырой тип (raw type) | Имя дженерик-типа без аргументов типа. Для List сырой тип — это List |
Ещё больше о дженериках, коллекциях и других элементах языка Java узнайте на нашем курсе «Профессия Java-разработчик». Научим программировать на самом востребованном языке и поможем устроиться на работу.
Переменные ссылочного типа хранят адрес ячейки в памяти, в которой лежит значение этой переменной.
В этом их ключевое отличие от примитивных типов, когда в переменной хранится само значение.
Все ссылочные типы в Java наследуются от типа Object.
Дженерики в Java для тех, кто постарше: стирание типов, наследование и принцип PECS
Рассказываем, как в любой непонятной ситуации правильно сочетать дженерик-типы.
В предыдущей статье «Дженерики для самых маленьких» мы рассказали о том, что такое дженерики (generics), зачем они нужны и как создавать дженерик-типы и методы. Там же говорили про ограничения (boundings) и wildcards. Без этих основ вам будет сложно разобраться с тем, что написано дальше. Поэтому освежите знания, если это необходимо.
Из этой статьи вы узнаете:
Фулстек-разработчик. Любимый стек: Java + Angular, но в хорошей компании готова писать хоть на языке Ада.
Почему ни один дженерик не доживает до выполнения программы
Воспользуемся примером из первой части рассказа о дженериках: там был класс Box — коробка для сбора мусора: можно было положить в неё или извлечь из неё только объект определённого типа:
Теперь создадим экземпляр такого класса и подставим вместо T конкретный тип: например, Paper — для коробки, в которую будем собирать бумагу:
Можно предположить, что теперь мы имеем дело с таким классом:
Эта запись помогает понять, как класс будет работать, но она не имеет ничего общего с тем, во что превращается дженерик-класс или интерфейс в результате компиляции.
Компилятор не генерирует class-файл для каждого параметризованного типа. Он создаёт один class-файл для дженерик-типа.
Компилятор стирает информацию о типе, заменяя все параметры без ограничений ( unbounded) типом Object, а параметры с границами ( bounded) — на эти границы. Это называется type erasure.
Кроме стирания (иногда говорят «затирания») типов, компилятор может добавлять приведение (cast) к нужному типу и создавать переходные bridge-методы, чтобы сохранить полиморфизм в классах-наследниках.
Пример 1. Стирание типа для дженерика без границ
Все параметры типов заменяются на Object. Вот что получится для нашего класса-коробки:
Пример 2. Стирание типа для дженерика с границами
Объявим дженерик-интерфейс c ограничением сверху ( upper bounding):
Вот что от этого останется после компиляции:
Пример 3. Bridge-метод
Создадим класс-наследник коробки для бумаги и переопределим в нём метод putItem:
Этому классу не всё равно, какого типа объекты приходят к нему в putItem, — нужно, чтобы они были типа Paper. Поэтому компилятору придётся немного докрутить класс — добавить в него bridge-метод с приведением типа:
А вот ещё несколько примеров дженерик-типов и того, что от них останется после компиляции:
До компиляции | После компиляции |
---|---|
> | Box |
Box | |
List [] | List[] |
Теперь, когда вы знаете про type erasure и его последствия, наверняка сможете ответить на вопрос, почему нельзя создать дженерик-Exception:
В каждом блоке try catch проверяется тип исключения, так как разные типы исключений могут обрабатываться по-разному. Для дженерик-исключения определить конкретный тип было бы невозможно, а потому компилятор даже не даст его создать. Это правило относится к классу Throwable и его наследникам.
Как создать наследника дженерик-класса
Пример 1. Класс-наследник — не дженерик.
Чтобы получить обычный, не дженерик-класс, мы должны вместо параметра T передать какой-то конкретный тип, что мы и сделали — передали Paper.
Пример 2. Класс-наследник и сам дженерик с тем же числом параметров.
Параметры у Box и SuperGenericBox не обязаны обозначаться буквой T (от type) — можно брать любую. В этом примере важно, чтобы буквы были одинаковые, иначе компилятор не разберётся.
Пример 3. Класс-наследник — дженерик с другим числом параметров.
Здесь уже не один, а два параметра. Один передадим родителю, а второй используем как-нибудь ещё — например, напишем метод newMethod с параметром этого нового типа.
У наследника класса-дженерика может быть сколько угодно параметров, включая ноль (когда это вовсе не дженерик). Главное — помнить про все параметры типа родительского класса и передать для каждого параметра конкретный тип или какой-то из параметров класса-наследника.
Что не так с дженерик-типами классов-наследников
В Java можно присвоить объект одного типа объекту другого типа, если типы совместимы: реализуют один и тот же интерфейс или лежат в одной цепочке наследования.
Например, PaperBox — наследник Box, и пример ниже успешно компилируется:
В терминах объектно-ориентированного программирования это называют отношением is a (является): бумажная коробка — это коробка (является коробкой). Или говорят, что PaperBox — это подтип (subtype) Box. При этом Box — супертип PaperBox.
Теперь возьмём не простую коробку, а её дженерик-вариант ( Box ), в которую будем класть разные типы мусора: Paper, Glass и тому подобные типы — наследники Garbage:
В этом случае в качестве аргумента типа можно выбрать как Garbage, так и его подтип:
Но что, если Box станет типом параметра метода? Сможем ли мы в этом случае передать другой дженерик-тип? Напишем простой пример:
И убедимся, что замена тут не пройдёт. Несмотря на то что Paper — подтип Garbage, Box
Дженерики инвариантны. Это означает, что, даже если A — подтип B, дженерик от A не является подтипом дженерика от B.
Для сравнения, массивы в Java ковариантны: если A — подтип B, A[] — подтип B[].
Как переопределить метод с дженерик-типами
Потренируемся сначала на простых типах и вспомним, что при переопределении методов необязательно полностью повторять сигнатуру родительского метода. Например, у таких методов могут различаться типы возвращаемых значений.
Переопределение будет правильным, если тип переопределённого метода — это подтип исходного метода. Например, так:
Добавим немного дженериков и применим то же правило:
Дженерики добавляют ещё пару возможностей для корректного переопределения. Оно будет верным, если:
Звучит сложно, так что лучше взглянем на код:
Правда, в обоих случаях компилятор покажет предупреждение о небезопасном использовании типов ( unchecked warning):
Note: GlassBox.java uses unchecked or unsafe operations.
Его можно понять: исходный метод требует, чтобы возвращался список объектов типа Garbage, а переопределённые хотят просто какой-то список. Там могут быть объекты типа Garbage, а могут и любые другие — вот компилятору и тревожно.
Зато если в исходном методе возвращаемый тип — с wildcard без ограничений, то при аналогичном переопределении предупреждений не будет:
При переопределении дженерик-методов с одинаковым числом параметров типа можно произвольно менять обозначения этих параметров:
В переопределённом методе параметр типа назван S, а не T, но переопределение остаётся корректным.
А вот ограничения для дженерика в переопределённом методе добавлять нельзя:
Получился не переопределённый метод, а просто метод с таким же названием.
Зато можно из дженерик-метода сделать обычный метод:
Компилятор спокоен, потому что метод в классе Box станет именно таким после type erasure — параметр типа будет заменён на Object.
Переопределение дженерик-метода будет корректно, если:
Как wildcards с ограничениями «портят» коллекции и зачем нужен принцип PECS
Если нужно что-то сделать с коллекциями объектов нескольких подтипов, удобны wildcards с ограничениями.
Например: List означает, что список может состоять из объектов типа Paper и всех его подтипов, а в List могут быть объекты типа Paper и всех супертипов — например, Garbage или Object.
С wildcards и коллекциями есть маленькая проблема — коллекции вроде тех, что в примере выше, нельзя использовать на полную катушку: свободно читать из них и записывать новые данные. Чтобы запомнить это ограничение, даже придумали принцип — принцип PECS.
PECS — Producer Extends, Consumer Super. Его суть:
Получается, в коллекцию с extends нельзя добавлять, а из коллекции с super нельзя читать? Вроде бы всё понятно, но давайте проверим:
Попробуем положить сюда экземпляр Paper — наследника Garbage:
Получим ошибку компиляции. Ладно, тогда, может, хотя бы объект типа Garbage подойдёт?
И снова нет. Принцип PECS не соврал — объект в такой список добавить нельзя. Единственное исключение — null. Вот так можно:
С первой частью принципа разобрались, теперь создадим коллекцию с ограничением снизу:
Добавим туда один объект типа Paper:
И попробуем его же прочитать. Если верить PECS, у нас это не должно получиться:
Но компилятору всё нравится — никаких ошибок нет. Проблемы, впрочем, начнутся, когда мы захотим сохранить полученное значение в переменной типа Paper или типа, совместимого с ним:
Вторая часть принципа PECS означает, что из коллекций, ограниченных снизу, нельзя без явного приведения типа (cast) прочитать объекты граничного класса, да и всех его родителей тоже. Единственное, что доступно, — тип Object:
К сожалению, принцип PECS ничего не говорит о том, какие объекты можно читать из producer, а какие добавлять в customer. Мы не придумали своего принципа, но сделали табличку, чтобы собрать вместе все правила:
Тип ограничения | Что можно читать | Что можно записывать |
---|---|---|
extends SomeType> | Объекты SomeType и всех его супертипов | Только null |
super SomeType> | Объекты типа Object | Объекты типа SomeType и всех его подтипов |
И даже картинку нарисовали:
Теперь точно не запутаетесь 🙂
Ещё больше хитростей дженериков и других особенностей Java — на курсе «Профессия Java-разработчик». Научим программировать на самом востребованном языке и поможем устроиться на работу.