Подробно о 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 list)

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

d95d71268067f57f837777610c475bdf.jpg

Какие объекты можно получить из List list?

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

public static void someMethod (List list) {    
	Class4 class4 = list.get(0); 
}

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

public static void someMethod (List list) {
    Class2 class2 = list.get(0);
}

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

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

Class4 class4 = list.get(0);

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

Class4 class4 = (Class4) list.get(0);

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

Почему в List list нельзя положить объекты суперклассов (Class0, Class1, Class2), думаю, очевидно: негоже объекту-наследнику ссылаться на объект-предок.  В List list нельзя положить объект, имеющий тип Number, можно только наоборот. Но что нам мешает, добавить в List list объект типа Class4 или Class5? Да всё тот же самый принцип! В момент компиляции JVM не знает, что во время выполнения программы будет скрываться под маской List. Может это будет List, а может быть List. И если это действительно окажется List, а вы будете добавлять туда элемент, имеющий тип Class3 или Class4, это будет равносильно тому, что вы добавляете элемент с типом Number в List. Вот, если бы компилятор был уверен, что List во время выполнения программы окажется либо листом элементов типа Class3, либо листом элементов типа-предка Class3, то он бы не возражал, против того, чтобы добавить в лист любых потомков Class3.

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

Из данного утверждения логично вытекает следующий вопрос: почему wildcard с super может принимать объекты, а wildcard с extend — нет? И на него мы уже практически нашли ответ выше. List — на деле может оказаться листом объектов самого «младшего» класса, тогда как конструкция List гарантирует, что при любом раскладе в листе будут объекты имеющие тип не «младше» класса Class3. Поэтому, следующий кусок кода скомпилируется

public static void someMethod (List list) {
    list.add(new Class4());
}

А такой, нет:

public static void someMethod (List list) {
    list.add(new Class2());
}

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

public static void someMethod (List list) {
    list.add(new Number());
}

Хорошо хоть, тот факт, что »wildcard с super —это consumer который предоставить ничего не может» — не нуждается в проверке и осмыслении. И так понятно, что следующий код не скомпилируется, ибо заповедано нам, что «consumer super…»

public static void someMethod (List list) {
    list.get(0);
}

Хотя, погодите-ка…

1744ac2e3bf88245cfd59a2b0c0940a3.jpg

Если забить данный код в IDE, мы увидим: несмотря на то, что наш многострадальный лист super, он не такой уж и consumer! Код успешно компилируется, следовательно, list.get(0) работает. Что же мы получим из листа? Может быть, мы можем получить объект типа Class3? Нет! Ну, тогда точно Class2 (он же всё-таки super). Опять мимо! Тогда остаётся Class4? И тут компилятор пошлёт нас учить матчасть. Но метод get() работает, следовательно что-то возвращает? А возвращает он объект самого «главного» класса в Java — класса Object.

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

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

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

public static void someMethod (List list) {
    Class2 obj = list.get(0);
}

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

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

© Habrahabr.ru