Java 泛型 PECS - 生产者extends消费者super

昨天,我正在研究一些 Java 集合 API,并且发现了两种主要用于将元素添加到集合中的方法。 他们俩都使用泛型语法来获取方法参数。 但是,第一种方法是使用<? super T>,而第二种方法是使用<? extends E>。 为什么?

首先,让我们看一下这两种方法的完整语法。

此方法负责将集合c的所有成员添加到另一个调用此方法的集合中。

boolean addAll(Collection<? extends E> c);

调用此方法可将“元素”添加到集合c

public static <T> boolean addAll(Collection<? super T> c, T... elements);

两者似乎都在做简单的事情,所以为什么它们都有不同的语法。 我们许多人可能会纳闷。 在这篇文章中,我试图揭开围绕它的概念的神秘性,该概念主要被称为 PECS (最早由 Joshua Bloch 在他的著作《Effective Java》中创造的术语)。

为什么要使用泛型通配符?

在我与 java 泛型 相关的上一篇文章中,我们了解到,泛型本质上用于类型安全性和不变式。 用例可以是 Integer 的列表,即List<Integer>。 如果您在 Java 中声明List<Integer>之类的列表,则 Java 保证它将检测并报告您将任何非整数类型插入上述列表的任何尝试。

但是很多时候,我们面临这样的情况,为了特定的目的,我们必须在方法中传递类的子类型或超类型作为参数。 在这些情况下,我们必须使用协变(缩小引用)和逆变(扩大引用)之类的概念。

了解<? extends T>

这是 PECS 的第一部分,即 PE(生产者extends。 为了将其与现实生活中的术语联系起来,让我们使用一篮子水果(即水果的集合)的类比。 当我们从篮子里摘水果时,我们要确保只取出水果而没有其他东西。 这样我们就可以编写如下泛型代码:

Fruit get = fruits.get(0);

在上述情况下,我们需要将水果的集合声明为List<? extends Fruit>。 例如:

class Fruit {
   @Override
   public String toString() {
      return "I am a Fruit !!";
   }
}

class Apple extends Fruit {
   @Override
   public String toString() {
      return "I am an Apple !!";
   }
}

public class GenericsExamples
{
   public static void main(String[] args)
   {
      //List of apples
      List<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 fruit 
      List<? extends Fruit> basket = apples;

      //Here we know that in basket there is nothing but fruit only
      for (Fruit fruit : basket)
      {
         System.out.println(fruit);
      }

      //basket.add(new Apple()); //Compile time error
      //basket.add(new Fruit()); //Compile time error
   }
}

查看上面的for循环。 它确保了从篮子里出来的任何东西都肯定会结出果实; 因此,您可以遍历它,然后简单地将其浇铸成水果。 现在,在最后两行中,我尝试在购物篮中添加AppleFruit,但是编译器不允许我添加。 为什么?

如果我们考虑一下,原因很简单。 <? extends Fruit>通配符告诉编译器我们正在处理水果类型的子类型,但是我们无法知道哪个水果,因为可能存在多个子类型。 由于没有办法说出来,而且我们需要保证类型安全(不变性),因此您不能在此类结构内放置任何内容。

另一方面,由于我们知道它可能是Fruit的子类型,因此可以从结构中获取数据,并保证它是Fruit

在上面的示例中,我们从集合List<? extends Fruit> basket中取出元素。所以这个篮子实际上是在生产水果。 简而言之,当您只想从集合中检索元素时,请将其视为生产者并使用<? extends T>语法。 “生产者extends”现在对您来说更有意义。

了解<? super T>

现在以不同的方式查看上述用例。 假设我们正在定义一个方法,在此方法中,我们只会在此购物篮中添加不同的水果。 就像我们在帖子“ addAll(Collection<? super T> c, T... elements)”开头看到的方法一样。 在这种情况下,篮子用于存储元素,因此应称为元素的使用者。

现在看下面的代码示例:

class Fruit {
   @Override
   public String toString() {
      return "I am a Fruit !!";
   }
}

class Apple extends Fruit {
   @Override
   public String toString() {
      return "I am an Apple !!";
   }
}

class AsianApple extends Apple {
   @Override
   public String toString() {
      return "I am an AsianApple !!";
   }
}

public class GenericsExamples
{
   public static void main(String[] args)
   {
      //List of apples
      List<Apple> apples = new ArrayList<Apple>();
      apples.add(new Apple());

      //We can assign a list of apples to a basket of apples
      List<? super Apple> basket = apples;

      basket.add(new Apple()); 		//Successful
      basket.add(new AsianApple()); //Successful
      basket.add(new Fruit()); 		//Compile time error
   }
}

我们可以在篮子内添加苹果,甚至是亚洲苹果,但不能在篮子中添加Fruit(苹果的超类型)。 为什么?

原因是购物篮是对Apple的超类商品列表的引用。 同样,我们不知道它是是哪个超类型,但是我们知道可以将Apple及其任何子类型(它们是Fruit的子类型)添加为没有问题(您可以随时在超类的集合中添加一个子类)。 因此,现在我们可以在购物篮中添加任何类型的Apple

如何从这种类型的数据中获取数据呢? 事实证明,您唯一可以使用的是Object实例:由于我们无法知道它是哪个超类型,因此编译器只能保证它将是对Object的引用,因为Object是任何 Java 类型的超类型。

在上面的示例中,我们将元素放入集合List<? super Apple> basket; 所以在这里,这个篮子实际上是在消耗苹果等元素。 简而言之,当您只想在集合中添加元素时,请将其视为使用者并使用<? super T>语法。 现在,“消费者super”对您也应该更有意义。

总结

基于上述推理和示例,让我们总结要点。

  1. 如果需要从集合中检索类型T的对象,请使用<? extends T>通配符。
  2. 如果需要将T类型的对象放入集合中,请使用<? super T>通配符。
  3. 如果您需要同时满足这两个条件,请不要使用任何通配符。 就这么简单。
  4. 简而言之,请记住术语 PECS。 生产者extends消费者super。 真的很容易记住。

这就是 Java 中泛型中简单而又复杂的概念的全部。 通过评论让我知道您的想法。