Что такое pecs java

Подробно о PECS

Всем привет. Сегодня я хочу поговорить о принципе PECS. Понимаю, что сейчас гуру программирования и многоопытные сеньоры в очередной раз впечатали ладонь в лицо, ибо «Java Generics появились в JDK 1.5, которая вышла 30 сентября 2004 года…». Но если есть те, для кого принцип PECS остаётся туманным и непонятным, а упорное гугленье только сгущает «туман», добро пожаловать под кат, будем вместе разбираться до полного духовного просветления. Хочу сразу предупредить, что в данной заметке не рассматривается, что такое дженерики и что такое wildcard. Если вы не знакомы с данными понятиями, то перед чтением необходимо с ними разобраться.

На первый взгляд кажется, что принцип PECS достаточно прост. Все, кто встречался с ним, знают, что это акроним, означающий «Producer Extends Consumer Super». Как объясняется в многочисленных статьях, если у нас есть некая коллекция, типизированная wildcard с верхней границей (extends) – то это, «продюсер». «Он только «продюсирует», предоставляет элемент из контейнера, а сам ничего не принимает». Если же у нас коллекция, типизированная wildcard по нижней границе (super) – то это, «потребитель», который «только принимает, а предоставить ничего не может».

Ну, вот собственно говоря и всё. Теперь мы овладели «тёмной магией» PECS и можем смело отправляться на собеседование в Гугл, чтобы сразить интервьюеров своей премудростью и многоопытностью. Остаётся одна небольшая формальность, запустить любимую IDE и для галочки убедиться, что всё так и есть: контейнеры, ограниченные по верхней границе, могут только предоставлять объекты, а контейнеры, ограниченные по нижней границе, являются только потребителями.

Для наглядности представим, что у нас есть иерархия классов, начинающаяся с Class0, который является предком для Class1, который в свою очередь является предком для Class2, и т.д. И есть некий метод, который принимает в качестве аргумента коллекцию, типизированную wildcard с верхней границей.

Согласно принципу PECS, мы не можем ничего положить в данный лист, он будет являться только поставщиком данных.

Что такое pecs java. Смотреть фото Что такое pecs java. Смотреть картинку Что такое pecs java. Картинка про Что такое pecs java. Фото Что такое pecs java

Досрочный ответ очевиден: объекты, содержащиеся в list, являются потомками Class3, следовательно, из list можно получить объекты Class4, Class5, Class6 и т.д. Если вы ответили именно так, то у меня для вас плохая новость – «очко уходит телезрителям»! Следующий кусок кода не скомпилируется:

Зато вот такой код будет корректным:

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

Но почему это так? Разве здесь нет противоречия? Лист содержит объекты-потомки для некоего класса, а получить из него мы можем только объекты, имеющие тип класса-предка. На самом деле, для компилятора здесь всё однозначно. Во время компиляции кода неизвестно, объекты какого именно класса будут содержаться в листе. Допустим, это будут объекты Class3. Тогда в этой строке

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

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

И тут мы плавно переходим ко второй части принципа PECS – «consumer super» («wildcard с super — это consumer, он только принимает, а предоставить ничего не может»).

Это то же самое, что написать:

Что такое pecs java. Смотреть фото Что такое pecs java. Смотреть картинку Что такое pecs java. Картинка про Что такое pecs java. Фото Что такое pecs java

И тут встаёт последний вопрос, который бы хотелось рассмотреть в рамках изучения PECS: почему из коллекции, типизированной wildecard с нижней границей, можно получить только объект класса Object?

Если вы внимательно прочитали всё, что написано выше, то уже, наверное, догадываетесь о том, каким будет ответ. Потому что конкретный тип объекта, типизированного wildcard будет известен только в момент выполнения программы, а коллекция, состоящая из объектов-потомков, не может содержать объекты-предки.

Если бы компилятор счёл данный код «легальным» (ведь Class2 является суперклассом для Class3)

то могла бы получится следующая ситуация: во время выполнения программы в метод будет передан List или вообще List (оба они соответствуют маске ) а объект-потомок (Class2 obj) будет ссылаться на предка ( list.get(0) ). Единственный способ этого избежать – получать из такого листа объект, имеющий тип, общий для всех других объектов в Java, то есть объект класса Object.

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

Источник

Использование 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, чтобы этих трудностей у вас было поменьше. Пригодится, если вы не гуру в дженериках, и поможет избежать много трудностей при погружении в тему.

Что такое pecs java. Смотреть фото Что такое pecs java. Смотреть картинку Что такое pecs java. Картинка про Что такое pecs java. Фото Что такое pecs java

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

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

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

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

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

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

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

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

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

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

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

Wildcards

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

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

Что такое pecs java. Смотреть фото Что такое pecs java. Смотреть картинку Что такое pecs java. Картинка про Что такое pecs java. Фото Что такое pecs 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

На скриншоте ниже два примера программы:
Что такое pecs java. Смотреть фото Что такое pecs java. Смотреть картинку Что такое pecs java. Картинка про Что такое pecs java. Фото Что такое pecs 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 подходит к концу. Вот другие источники, которые помогут вам в освоении темы:

Источник

Дженерики в Java для тех, кто постарше: стирание типов, наследование и принцип PECS

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

Что такое pecs java. Смотреть фото Что такое pecs java. Смотреть картинку Что такое pecs java. Картинка про Что такое pecs java. Фото Что такое pecs java

Что такое pecs java. Смотреть фото Что такое pecs java. Смотреть картинку Что такое pecs java. Картинка про Что такое pecs java. Фото Что такое pecs java

В предыдущей статье «Дженерики для самых маленьких» мы рассказали о том, что такое дженерики (generics), зачем они нужны и как создавать дженерик-типы и методы. Там же говорили про ограничения (boundings) и wildcards. Без этих основ вам будет сложно разобраться с тем, что написано дальше. Поэтому освежите знания, если это необходимо.

Из этой статьи вы узнаете:

Что такое pecs java. Смотреть фото Что такое pecs java. Смотреть картинку Что такое pecs java. Картинка про Что такое pecs java. Фото Что такое pecs java

Фулстек-разработчик. Любимый стек: 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[].

Что такое pecs java. Смотреть фото Что такое pecs java. Смотреть картинку Что такое pecs java. Картинка про Что такое pecs java. Фото Что такое pecs java

Как переопределить метод с дженерик-типами

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

Переопределение будет правильным, если тип переопределённого метода — это подтип исходного метода. Например, так:

Добавим немного дженериков и применим то же правило:

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

Звучит сложно, так что лучше взглянем на код:

Правда, в обоих случаях компилятор покажет предупреждение о небезопасном использовании типов ( 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 и всех его подтипов

И даже картинку нарисовали:

Что такое pecs java. Смотреть фото Что такое pecs java. Смотреть картинку Что такое pecs java. Картинка про Что такое pecs java. Фото Что такое pecs java

Теперь точно не запутаетесь 🙂

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

Источник

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

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