什么是PECS(生产者扩大消费者超级)?

在阅读generics时,我遇到了PECS( Producer extends和Consumer super )。

有人可以向我解释如何使用PECS解决extendssuper之间的混淆?

tl; dr: “PECS”来自collections的观点。 如果您只是从通用集合中提取项目,那么它是一个生产者,您应该使用extends ; 如果你只是填充物品,它是一个消费者,你应该使用super 。 如果你使用相同的集合,你不应该使用extends或者super


假设你有一个方法把参数作为一个集合,但是你希望它比接受一个Collection<Thing>更加灵活。

案例1:你想通过收集,并与每个项目的事情。
那么这个清单是一个生产者 ,所以你应该使用一个Collection<? extends Thing> Collection<? extends Thing>

推理是一个Collection<? extends Thing> Collection<? extends Thing>可以包含Thing任何子types,因此当您执行操作时,每个元素将performance为Thing 。 (你实际上不能向Collection<? extends Thing>添加任何Collection<? extends Thing> ,因为你不能在运行时知道集合所持有的Thing特定子types。)

情况2:你想添加的东西到collections。
那么这个列表是一个消费者 ,所以你应该使用一个Collection<? super Thing> Collection<? super Thing>

这里的推理是不像Collection<? extends Thing> Collection<? extends Thing>Collection<? super Thing> Collection<? super Thing>总是可以持有一Thing不pipe实际的参数化types是什么。 这里你不在乎列表中已经有的Thing ,只要它能够添加一个Thing 。 这是什么? super Thing ? super Thing保证。

“计算机科学”背后的原理就是以此命名的

  • 协变 – ? 扩展MyClass,
  • 逆变 – – ? 超级MyClass和
  • Invariance / non-Variance – MyClass

下面的图片应该解释这个概念。

图片提供: Andrey Tyukin

协变与逆变

PECS(“ 生产者extendssuper消费者 ”的缩写)可以用“ 获取和放置原则 ”来解释

获取和放置原则(来自Javagenerics和集合)

它指出,

  1. 当你只结构中获取值时,使用扩展通配符
  2. 当你只值放入一个结构中时,使用超级通配符
  3. 并且当你们都得到和放置不要使用通配符

让我们通过例子来理解它:

1.对于扩展通配符(获取值,即生产者extends

这是一个方法,它需要一个数字的集合,将每个转换为一个double精度数,并将它们相加

 public static double sum(Collection<? extends Number> nums) { double s = 0.0; for (Number num : nums) s += num.doubleValue(); return s; } 

我们来调用这个方法:

 List<Integer>ints = Arrays.asList(1,2,3); assert sum(ints) == 6.0; List<Double>doubles = Arrays.asList(2.78,3.14); assert sum(doubles) == 5.92; List<Number>nums = Arrays.<Number>asList(1,2,2.78,3.14); assert sum(nums) == 8.92; 

由于sum()方法使用extends ,所有以下调用都是合法的。 如果不使用扩展,前两个调用将不合法。

例外不能将任何东西放入用extends通配符声明的types中 – 除了属于每个引用types的值null之外:

 List<Integer> ints = new ArrayList<Integer>(); ints.add(1); ints.add(2); List<? extends Number> nums = ints; nums.add(null); // ok assert nums.toString().equals("[1, 2, null]"); 

2.对于超级通配符(把值,即super消费者)

这里是一个方法,它需要一个数字和一个int n的集合,并将从零开始的前n整数放入集合中:

 public static void count(Collection<? super Integer> ints, int n) { for (int i = 0; i < n; i++) ints.add(i); } 

我们来调用这个方法:

 List<Integer>ints = new ArrayList<Integer>(); count(ints, 5); assert ints.toString().equals("[0, 1, 2, 3, 4]"); List<Number>nums = new ArrayList<Number>(); count(nums, 5); nums.add(5.0); assert nums.toString().equals("[0, 1, 2, 3, 4, 5.0]"); List<Object>objs = new ArrayList<Object>(); count(objs, 5); objs.add("five"); assert objs.toString().equals("[0, 1, 2, 3, 4, five]"); 

因为count()方法使用super ,所以下面的所有调用都是合法的:如果super不被使用,最后两个调用将不合法。

例外 :你不能supertypes声明的types中取出任何东西 – 除了Objecttypes的值,它是每个引用types的超types:

 List<Object> objs = Arrays.<Object>asList(1,"two"); List<? super Integer> ints = objs; String str = ""; for (Object obj : ints) str += obj.toString(); assert str.equals("1two"); 

3.同时获取和放置时,请勿使用通配符

无论何时您值放入并从同一结构中获取值, 都不应使用通配符

 public static double sumCount(Collection<Number> nums, int n) { count(nums, n); return sum(nums); } 
 public class Test { public class A {} public class B extends A {} public class C extends B {} public void testCoVariance(List<? extends B> myBlist) { B b = new B(); C c = new C(); myBlist.add(b); // does not compile myBlist.add(c); // does not compile A a = myBlist.get(0); } public void testContraVariance(List<? super B> myBlist) { B b = new B(); C c = new C(); myBlist.add(b); myBlist.add(c); A a = myBlist.get(0); // does not compile } } 

PECS(生产者extends和消费者super

助记符 – >input和输出(即返回types原则)

在Java中,参数和types参数不支持inheritance,如下所示。

 class Super{ void testCoVariance(Object parameter){} // method Consumes the Object Object testContraVariance(){ return null;} //method Produces the Object } class Sub extends Super{ @Override void testCoVariance(String parameter){} //doesn't support eventhough String is subtype of Object @Override String testContraVariance(){ return null;}//compiles successfully ie return type is don't care } 

genericstypes参数:

 class Shape { } class DrawShape{ void draw(ArrayList<Shape> al){ } } class DrawObject{ void draw(ArrayList<Object> al){ } } public class Test { public static void main(String[] args) { ArrayList rawArrayList=new ArrayList(); ArrayList<Object> arrayListObject=new ArrayList<>(); ArrayList<Shape> arrayListShape=new ArrayList<>(); DrawShape drawShape=new DrawShape(); drawShape.draw(rawArrayList); // compiles drawShape.draw(arrayListObject); // Error: the type DrawShape is not applicable for the arguments drawShape.draw(arrayListShape); // compiles DrawObject drawObject = new DrawObject(); drawObject.draw(rawArrayList); // compiles drawObject.draw(arrayListObject); // compiles drawObject.draw(arrayListShape); // Error: the type DrawObject is not applicable for the arguments } } 

通配符通配符解决了这些问题:

注意:通配符? 意味着零次或一次

通配符描述了一系列types。 有三种不同的通配符:

  • 方差/无方差: ? 还是? extends Object ? extends Object无界通配符。 它代表所有types的家庭。 (同方差/非方差)
  • 协variables: ? extends T ? extends T (获取值,即生产者extends ) – 通配符的上限 。 它代表所有types的TT亚型,包括T型。 必须是一个特定types的祖先。你不能添加元素到集合中,只能读取它们(协方差)
  • 反差: ? super T ? super T (Put values,即super消费者) – 具有下限的通配符。 它代表所有typesT超types的家族,包括T型。 必须扩展一个特定的types,你不能读取要收集的元素,只能添加它们。(contra-variance)

在这里输入图像描述

 class Shape { void draw() { } } class Circle extends Shape { void draw() { } } class Square extends Shape { void draw() { } } class Rectangle extends Shape { void draw() { } } public class TestContraVariance { /* * Example for an upper bound wildcard (Get values ie Producer `extends`) * * */ public void testCoVariance(List<? extends Shape> list) { list.add(new Shape()); // Error: is not applicable for the arguments (Shape) ie inheritance is not supporting list.add(new Circle()); // Error: is not applicable for the arguments (Circle) ie inheritance is not supporting list.add(new Square()); // Error: is not applicable for the arguments (Square) ie inheritance is not supporting list.add(new Rectangle()); // Error: is not applicable for the arguments (Rectangle) ie inheritance is not supporting Shape shape= list.get(0);//compiles so list act as produces only /*You can't add a Shape,Circle,Square,Rectangle to a List<? extends Shape> * You can get an object and know that it will be an Shape */ } /* * Example for a lower bound wildcard (Put values ie Consumer`super`) * */ public void testContraVariance(List<? super Shape> list) { list.add(new Shape());//compiles ie inheritance is supporting list.add(new Circle());//compiles ie inheritance is supporting list.add(new Square());//compiles ie inheritance is supporting list.add(new Rectangle());//compiles ie inheritance is supporting Shape shape= list.get(0); // Error: Type mismatch, so list acts only as consumer /*You can add a Shape,Circle,Square,Rectangle to a List<? extends Shape> * You can't get an object and don't know what kind of Shape it is. */ } } 

Angelika Langer最好学习仿制药

通配符使用指南

学习使用generics进行编程时,更令人困惑的一个方面是确定何时使用上界有界的通配符,何时使用下界有界的通配符。

为了讨论的目的,将variables看作提供以下两个函数之一是有帮助的:

一个“In”variables
一个“in”variables将数据提供给代码。 想象一下有两个参数的复制方法: copy(src, dest)src参数提供要复制的数据,所以它是“in”参数。

一个“出”variables
“out”variables保存用于其他地方的数据。 在复制示例中, copy(src, dest)dest参数接受数据,所以它是“out”参数。


通配符指南:

  • 一个“in”variables用一个上界的通配符来定义,使用extends关键字。
  • 使用super关键字定义一个“out”variables,其下限为通配符。
  • 在可以使用Object类中定义的方法访问“in”variables的情况下,使用无界通配符。
  • 在代码需要以“in”和“out”variables访问variables的情况下,不要使用通配符。

资源

比喻: 来源

协变与马匹与人的对立

在下图中,typesA是一组马,typesB是一组人,箭头描述了Function1 [A,B]types的函数。

方差告诉我,我可以使用这个函数的types比函数1 [A,B]更灵活,我可以使用任何types的Function1 [X,Y],其中X是A的一个子集,Y是B.“在这种types中使用”意味着我在我知道的情况下(或者说,编译器可以certificate)传递给函数的参数始终是集合X的成员,并且代码处理函数的结果可以处理任何属于集合Y的成员的值。如下图所示:

在这里输入图像描述

但是Function1 [-A,+ B]的完整types包含这些有趣的小符号,表示方差,减号表示它在A中是逆变的,在B中是正协变的加号意思。

方差告诉我,我可以使用这个函数的types比函数1 [A,B]更灵活,我可以使用任何types的Function1 [X,Y],其中X是A的一个子集,Y是B.“在这种types中使用”意味着我在我知道的情况下(或者说,编译器可以certificate)传递给函数的参数始终是集合X的成员,并且代码处理函数的结果可以处理任何属于集合Y的成员的值。如下图所示:
在这里输入图像描述

如果我把function限制在棕色的马上,它一点也不会伤害,它总是会为那匹马返回一个明确定义的人。 但是如果我和一头牛一起打电话就会失败。

正如我在回答另一个问题时解释的那样,PECS是由Josh Bloch创build的助记符,用于帮助记忆产品的使用者。

这意味着当传递给方法的参数化types将产生 T实例(它们将以某种方式从中检索) ? extends T ? extends T应该被使用,因为T的子类的任何实例也是T

当传递给方法的参数化types将消耗 T实例(它们将被传递给它来做某事) ? super T 应该使用? super T因为T一个实例可以合法地传递给任何接受T超types的方法。 例如,一个Comparator<Number>可以在一个Collection<Integer>使用。 ? extends T 由于Comparator<Integer>无法在Collection<Number>上运行,所以? extends T将无法工作。

请注意,一般你应该只使用? extends T ? extends T? super T ? super T为某些方法的参数。 方法应该使用T作为generics返回types的types参数。

简而言之,容易记住PECS

  1. 使用<? extends T> 如果需要从集合中检索typesT对象,则<? extends T>通配符。
  2. 使用<? super T> <? super T>通配符,如果你需要把typesT对象在集合中。
  3. 如果你需要同时满足这两件事,那么不要使用任何通配符。 尽可能简单。

(添加一个答案,因为没有足够的generics通配符的例子)

  // Source List<Integer> intList = Arrays.asList(1,2,3); List<Double> doubleList = Arrays.asList(2.78,3.14); List<Number> numList = Arrays.asList(1,2,2.78,3.14,5); // Destination List<Integer> intList2 = new ArrayList<>(); List<Double> doublesList2 = new ArrayList<>(); List<Number> numList2 = new ArrayList<>(); // Works copyElements1(intList,intList2); // from int to int copyElements1(doubleList,doublesList2); // from double to double static <T> void copyElements1(Collection<T> src, Collection<T> dest) { for(T n : src){ dest.add(n); } } // Let's try to copy intList to its supertype copyElements1(intList,numList2); // error, method signature just says "T" // and here the compiler is given // two types: Integer and Number, // so which one shall it be? // PECS to the rescue! copyElements2(intList,numList2); // possible // copy Integer (? extends T) to its supertype (Number is super of Integer) private static <T> void copyElements2(Collection<? extends T> src, Collection<? super T> dest) { for(T n : src){ dest.add(n); } } 
Interesting Posts