Подробно о 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 с верхней границей.
someMethod (List<? extends Class3> list)
Согласно принципу PECS, мы не можем ничего положить в данный лист, он будет являться только поставщиком данных.
Какие объекты можно получить из List<? extends Class3> list
?
Досрочный ответ очевиден: объекты, содержащиеся в list, являются потомками Class3, следовательно, из list можно получить объекты Class4, Class5, Class6 и т.д. Если вы ответили именно так, то у меня для вас плохая новость – «очко уходит телезрителям»! Следующий кусок кода не скомпилируется:
public static void someMethod (List<? extends Class3> list) {
Class4 class4 = list.get(0);
}
Зато вот такой код будет корректным:
public static void someMethod (List<? extends Class3> list) {
Class2 class2 = list.get(0);
}
Отсюда неочевидный вывод: из данного списка можно получить только объекты суперклассов.
Но почему это так? Разве здесь нет противоречия? Лист содержит объекты-потомки для некоего класса, а получить из него мы можем только объекты, имеющие тип класса-предка. На самом деле, для компилятора здесь всё однозначно. Во время компиляции кода неизвестно, объекты какого именно класса будут содержаться в листе. Допустим, это будут объекты Class3. Тогда в этой строке
Class4 class4 = list.get(0);
мы получим объект класса-предка и попытаемся положить его в переменную, имеющую тип класса-потомка, чего Java конечно же сделать неявно не позволит. Впрочем, если мы гарантируем компилятору, что точно знаем, объекты какого класса будут лежать в данном листе, то легко сможем с ним договориться.
Class4 class4 = (Class4) list.get(0);
С этим разобрались. Но почему мы не можем ничего положить в данный лист? Какая религия запрещает нам это делать?
Почему в List<? extends Class3> list
нельзя положить объекты суперклассов (Class0, Class1, Class2), думаю, очевидно: негоже объекту-наследнику ссылаться на объект-предок. В List<Integer> list
нельзя положить объект, имеющий тип Number,
можно только наоборот. Но что нам мешает, добавить в List<? extends Class3> list
объект типа Class4 или Class5? Да всё тот же самый принцип! В момент компиляции JVM не знает, что во время выполнения программы будет скрываться под маской List<? extends Class3>
. Может это будет List<Class4>
, а может быть List<Class100500>
. И если это действительно окажется List<Class100500>
, а вы будете добавлять туда элемент, имеющий тип Class3 или Class4, это будет равносильно тому, что вы добавляете элемент с типом Number
в List<Integer>
. Вот, если бы компилятор был уверен, что List<? extends Class3>
во время выполнения программы окажется либо листом элементов типа Class3, либо листом элементов типа-предка Class3, то он бы не возражал, против того, чтобы добавить в лист любых потомков Class3.
И тут мы плавно переходим ко второй части принципа PECS – «consumer super» («wildcard с super — это consumer, он только принимает, а предоставить ничего не может»).
Из данного утверждения логично вытекает следующий вопрос: почему wildcard с super может принимать объекты, а wildcard с extend – нет? И на него мы уже практически нашли ответ выше. List<? extends Class3>
- на деле может оказаться листом объектов самого «младшего» класса, тогда как конструкция List<? super Class3>
гарантирует, что при любом раскладе в листе будут объекты имеющие тип не «младше» класса Class3. Поэтому, следующий кусок кода скомпилируется
public static void someMethod (List<? super Class3> list) {
list.add(new Class4());
}
А такой, нет:
public static void someMethod (List<? super Class3> list) {
list.add(new Class2());
}
Это то же самое, что написать:
public static void someMethod (List<Integer> list) {
list.add(new Number());
}
Хорошо хоть, тот факт, что «wildcard с super —это consumer который предоставить ничего не может» - не нуждается в проверке и осмыслении. И так понятно, что следующий код не скомпилируется, ибо заповедано нам, что «consumer super…»
public static void someMethod (List<? super Class3> list) {
list.get(0);
}
Хотя, погодите-ка…
Если забить данный код в IDE, мы увидим: несмотря на то, что наш многострадальный лист super, он не такой уж и consumer! Код успешно компилируется, следовательно, list.get(0)
работает. Что же мы получим из листа? Может быть, мы можем получить объект типа Class3? Нет! Ну, тогда точно Class2 (он же всё-таки super). Опять мимо! Тогда остаётся Class4? И тут компилятор пошлёт нас учить матчасть. Но метод get()
работает, следовательно что-то возвращает? А возвращает он объект самого «главного» класса в Java - класса Object.
И тут встаёт последний вопрос, который бы хотелось рассмотреть в рамках изучения PECS: почему из коллекции, типизированной wildecard с нижней границей, можно получить только объект класса Object?
Если вы внимательно прочитали всё, что написано выше, то уже, наверное, догадываетесь о том, каким будет ответ. Потому что конкретный тип объекта, типизированного wildcard будет известен только в момент выполнения программы, а коллекция, состоящая из объектов-потомков, не может содержать объекты-предки.
Если бы компилятор счёл данный код «легальным» (ведь Class2 является суперклассом для Class3)
public static void someMethod (List<? super Class3> list) {
Class2 obj = list.get(0);
}
то могла бы получится следующая ситуация: во время выполнения программы в метод будет передан List<Class1>
или вообще List<Object>
(оба они соответствуют маске <? super Class3>
) а объект-потомок (Class2 obj) будет ссылаться на предка (list.get(0)
). Единственный способ этого избежать – получать из такого листа объект, имеющий тип, общий для всех других объектов в Java, то есть объект класса Object.
Вот и всё, что хотелось бы рассказать о принципе PECS. Надеюсь, моё объяснение вышло понятным и поможет страждущим истины разобраться в данном вопросе.