Вчера я просматривал некоторые API коллекций Java и обнаружил два метода, которые в основном использовались для добавления элементов в коллекцию. Они оба использовали синтаксис generics для получения аргументов метода. Однако первый метод использовал <? super T>, тогда как второй метод использовал <? extends E>. Почему?
1. Синтаксис
Давайте сначала рассмотрим полный синтаксис обоих методов.
Этот метод отвечает за добавление всех членов коллекции «c» в другую коллекцию, где вызывается этот метод.
boolean addAll(Collection<? extends E> c);
Этот метод вызывается для добавления «элементов» в коллекцию «c».
public static <T> boolean addAll(Collection<? super T> c, T... elements);
Оба, кажется, делают простую вещь, так почему же у них разный синтаксис? Многие из нас могут задаться вопросом. В этом посте я пытаюсь развеять мифы о концепции вокруг него, которая в первую очередь называется PECS(термин, впервые введенный Джошуа Блохом в его книге Effective Java).
2. Дженерики с подстановочными знаками
В моем последнем посте, посвященном дженерикам Java, мы узнали, что дженерики используются для обеспечения безопасности типов и по своей природе инвариантны. Примером использования может быть список Integer, т. е. List<Integer>. Если вы объявляете список как List<Integer>, то Java гарантирует, что обнаружит и сообщит вам о любой попытке вставить любой нецелочисленный тип в указанный выше список.
Но часто мы сталкиваемся с ситуациями, когда нам нужно передать подтип или супертип класса в качестве аргумента в методе для определенных целей. В этих случаях нам приходится использовать такие концепции, как ковариация(сужение ссылки) и контрвариация(расширение ссылки).
3. Понимание «Producer Extends» или <? extends T>
Это первая часть PECS, то есть PE(Producer extends). Чтобы связать это с реальными терминами, давайте используем аналогию с корзиной фруктов(т.е. сбор фруктов). Когда мы выбираем фрукты из корзины, то мы хотим быть уверены, что вынимаем только фрукты и ничего больше; так что мы можем написать общий код, например, так:
Fruit fruit = fruits.get(0);
В приведенном выше случае нам необходимо объявить коллекцию фруктов как List<? extends Fruit>. Например:
class Fruit {@Overridepublic String toString() {return "I am a Fruit !!";}}class Apple extends Fruit {@Overridepublic String toString() {return "I am an Apple !!";}}public class GenericsExamples{public static void main(String[] args){//List of applesList<Apple> apples = new ArrayList<Apple>();apples.add(new Apple());//We can assign a list of apples to a basket of fruits;//because apple is subtype of fruitList<? extends Fruit> basket = apples;//Here we know that in basket there is nothing but fruit onlyfor(Fruit fruit : basket){System.out.println(fruit);}//basket.add(new Apple()); //Compile time error//basket.add(new Fruit()); //Compile time error}}
Посмотрите на цикл for выше. Он гарантирует, что все, что выпадет из корзины, определенно будет фруктом; поэтому вы проходите по нему и просто приводите его к типу Fruit. Теперь в последних двух строках я попытался добавить в корзину Apple, а затем Fruit, но компилятор не позволил мне этого сделать. Почему?
Причина довольно проста, если подумать; подстановочный знак <? extends Fruit> сообщает компилятору, что мы имеем дело с подтипом типа Fruit, но мы не можем знать, с каким именно фруктом, поскольку может быть несколько подтипов. Поскольку нет способа узнать, и нам нужно гарантировать безопасность типов(инвариантность), вам не разрешат поместить что-либо внутрь такой структуры.
С другой стороны, поскольку мы знаем, что какой бы тип это ни был, он будет подтипом Fruit, мы можем извлечь данные из структуры с гарантией того, что это будет Fruit.
В примере выше мы извлекаем элементы из коллекции «List<? extends Fruit> basket»; так что здесь эта корзина фактически производит элементы, т. е. фрукты. Проще говоря, когда вы хотите ТОЛЬКО извлечь элементы из коллекции, относитесь к ней как к производителю и используйте синтаксис «? extends T>». «Производитель расширяет» теперь должно иметь для вас больше смысла.
4. Понимание «Потребительского супер», т.е. <? super T>
Теперь посмотрим на вышеприведенный пример использования по-другому. Предположим, мы определяем метод, в котором мы будем добавлять только разные фрукты в эту корзину. Так же, как мы видели метод в начале поста «addAll(Collection<? super T> c, T… elements)». В таком случае корзина используется для хранения элементов, поэтому ее следует называть потребителем элементов.
Теперь посмотрите на пример кода ниже:
class Fruit {@Overridepublic String toString() {return "I am a Fruit !!";}}class Apple extends Fruit {@Overridepublic String toString() {return "I am an Apple !!";}}class AsianApple extends Apple {@Overridepublic String toString() {return "I am an AsianApple !!";}}public class GenericsExamples{public static void main(String[] args){//List of applesList<Apple> apples = new ArrayList<Apple>();apples.add(new Apple());//We can assign a list of apples to a basket of applesList<? super Apple> basket = apples;basket.add(new Apple()); //Successfulbasket.add(new AsianApple()); //Successfulbasket.add(new Fruit()); //Compile time error}}
Мы можем добавлять яблоки и даже азиатские яблоки в корзину, но мы не можем добавлять в корзину Фрукты(супертип Яблока). Почему?
Причина в том, что корзина ссылается на список чего-то, что является супертипом Apple. Опять же, мы не можем знать, какой это супертип, но мы знаем, что Apple и любой из его подтипов(которые являются подтипом Fruit) могут быть добавлены без проблем(вы всегда можете добавить подтип в коллекцию супертипа). Итак, теперь мы можем добавить любой тип Apple в корзину.
А как насчет получения данных из такого типа? Оказывается, единственное, что можно из него получить, это экземпляры Object: поскольку мы не можем знать, какой это супертип, компилятор может гарантировать только то, что это будет ссылка на Object, поскольку Object является супертипом любого типа Java.
В примере выше мы помещаем элементы в коллекцию «List<? super Apple> basket»; так что здесь эта корзина фактически потребляет элементы, т. е. яблоки. Проще говоря, когда вы хотите ТОЛЬКО добавлять элементы в коллекцию, относитесь к ней как к потребителю и используйте синтаксис «? super T>». Теперь «Consumer super» также должен иметь для вас больше смысла.
5. Резюме
На основе приведенных выше рассуждений и примеров давайте подведем итоги нашего обучения в виде маркированного списка.
- Используйте подстановочный знак <? extends T>, если вам необходимо извлечь объект типа T из коллекции.
- Используйте подстановочный знак <? super T>, если вам нужно поместить объекты типа T в коллекцию.
- Если вам нужно удовлетворить обе эти потребности, ну, не используйте никаких подстановочных знаков. Это так просто.
- Короче, запомните термин PECS. Производитель расширяет Потребителя супер. Действительно легко запомнить.
Это все для простых, но сложных концепций в дженериках в Java. Дайте мне знать ваши мысли в комментариях.