Java Generics PECS 之 生产者扩展了超级消费者

昨天,我正在研究一些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)在他的《有效Java》一书中首次创造的术语)。

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

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

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

了解<?扩展T>

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

水果得到= 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循环。它确保了从篮子里出来的任何东西都肯定会结出果实;因此,您可以遍历它,然后简单地将其浇铸成水果。现在,在最后两行中,我尝试先添加一个Apple,然后再添加一个Fruitin篮子,但是编译器不允许我这样做。为什么?

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

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

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

了解<?超级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的子类型)添加为没有问题(您可以始终在supertype的集合中添加一个子类型)。因此,现在我们可以在购物篮中添加任何类型的Apple。

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

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

摘要

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

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

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

学习愉快!

saigon has written 1440 articles

Leave a Reply