关于Java:什么是PECs(Producer Extends Consumer Super))?

What is PECS (Producer Extends Consumer Super)?

我在读仿制药时遇到了PEC(生产商extends和消费者super)。

有人能给我解释一下如何使用PEC来解决extendssuper之间的混淆吗?


tl;dr:"pecs"是从集合的角度来看的。如果您只是从一般集合中提取项目,那么它是一个生产者,您应该使用extends;如果您只是填充项目,那么它是一个消费者,您应该使用super。如果两种方法都使用同一个集合,则不应使用extendssuper

假设您有一个方法,它将一组事物作为参数,但您希望它比只接受一个Collection更灵活。

案例1:你想通过收集来处理每一个项目。然后列表是一个生产者,所以您应该使用Collection

理由是,Collection可以容纳Thing的任何子类型,因此在执行操作时,每个元素都将表现为Thing。(实际上,您不能向Collection添加任何内容,因为您不能在运行时知道集合所包含的Thing的特定子类型。)

案例2:您想向集合中添加内容。那么这个列表就是一个消费者,所以您应该使用Collection

这里的理由是,与Collection不同,Collection可以始终保持Thing,不管实际的参数化类型是什么。在这里,你不在乎清单中已经有什么,只要它允许添加一个Thing;这是? super Thing的保证。


在计算机科学中,这背后的原理被称为

  • 协方差:? extends MyClass
  • 反向:? super MyClass
  • 不变性/非方差:MyClass

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

图片提供者:Andrey Tyukin

Covariance vs Contravariance


PECS(生产者extends和消费者super的简称)可以解释为:即得即用原则。

获取和放置原则(来自Java泛型和集合)

它说,

  • 仅从结构中获取值时使用扩展通配符
  • 仅将值放入结构时使用超级通配符
  • 当你得到和得到的时候不要使用通配符。
  • 让我们举例来理解它:

    1。用于扩展通配符(获取值,即生产者extends)

    这里有一个方法,它获取一组数字,将每个数字转换成一个double,并将它们相加。

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

    让我们调用这个方法:

    1
    2
    3
    4
    5
    6
    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通配符声明的类型中,除非值null属于每个引用类型:

    1
    2
    3
    4
    5
    6
    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。对于超级通配符(Put值,即消费者super)

    这里有一种方法,它取一组数字和一个int n并将第一个n整数从零开始放入集合中:

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

    让我们调用这个方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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,最后两个调用将是不合法的。

    例外:不能从用super通配符声明的类型中获取任何内容,除非类型为Object的值是每个引用类型的父类型:

    1
    2
    3
    4
    5
    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");

    三。当get和put都使用时,不要使用通配符

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

    1
    2
    3
    4
    public static double sumCount(Collection<Number> nums, int n) {
       count(nums, n);
       return sum(nums);
    }

    PECS(生产者extends和消费者super)

    助记法→取放原理。

    该原则规定:

    • 仅从结构中获取值时,使用扩展通配符。
    • 仅将值放入结构时使用超级通配符。
    • 当你得到和得到的时候不要使用通配符。

    Java中的示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Super {

        Object testCoVariance(){ return null;} //Covariance of return types in the subtype.
        void testContraVariance(Object parameter){} // Contravariance of method arguments in the subtype.
    }

    class Sub extends Super {

        @Override
        String testCoVariance(){ return null;} //compiles successfully i.e. return type is don't care(String is subtype of Object)
        @Override
        void testContraVariance(String parameter){} //doesn't support even though String is subtype of Object

    }

    Liskov替换原则:如果s是t的子类型,则t类型的对象可以替换为s类型的对象。

    在编程语言的类型系统中,输入规则

    • 协变的,如果它保持类型的顺序(≤),它将类型从更具体的排序到更一般的排序;
    • 相反的,如果它颠倒了这个顺序;
    • 如果两者都不适用,则为不变的或非不变的。

    协变与逆变

    • 只读数据类型(源)可以是协变的;
    • 只写数据类型(接收器)可以是反向的。
    • 同时充当源和接收器的可变数据类型应该是不变的。

    为了说明这一一般现象,请考虑数组类型。对于类型动物,我们可以制作类型动物[]

    • 协变:猫是动物;
    • 相反:动物是猫;
    • 不变式:动物[]不是猫[],猫[]不是动物[]。

    Java实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Object name= new String("prem"); //works
    List<Number> numbers = new ArrayList<Integer>();//gets compile time error

    Integer[] myInts = {1,2,3,4};
    Number[] myNumber = myInts;
    myNumber[0] = 3.14; //attempt of heap pollution i.e. at runtime gets java.lang.ArrayStoreException: java.lang.Double(we can fool compiler but not run-time)

    List<String> list=new ArrayList<>();
    list.add("prem");
    List<Object> listObject=list; //Type mismatch: cannot convert from List<String> to List<Object> at Compiletime

    更多例子

    有界(即朝某个地方)通配符:有3种不同的通配符味道:

    • 方差/非方差:?? extends Object无界通配符。它代表所有类型的家庭。当你得到并投入时使用。
    • 共同方差:? extends T(T的子类型的所有类型的家族),一个带有上界的通配符。T是继承层次中最上层的类。仅从结构中获取值时,请使用extends通配符。
    • 反向方差:? super T(T的父类型的所有类型的族)-带有下界的通配符。T是继承层次中最低级的类。当只将值放入结构中时,使用super通配符。

    注:通配符?表示零或一次,表示未知类型。通配符可以用作参数的类型,而不能用作泛型方法调用、泛型类实例创建的类型参数。(即,当使用通配符时,该引用在程序中的其他地方(如我们使用T)中未使用)

    enter image description here

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    class Shape { void draw() {}}

    class Circle extends Shape {void draw() {}}

    class Square extends Shape {void draw() {}}

    class Rectangle extends Shape {void draw() {}}

    public class Test {
     /*
       * Example for an upper bound wildcard (Get values i.e Producer `extends`)
       *
       * */
     

        public void testCoVariance(List<? extends Shape> list) {
            list.add(new Shape()); // Error:  is not applicable for the arguments (Shape) i.e. inheritance is not supporting
            list.add(new Circle()); // Error:  is not applicable for the arguments (Circle) i.e. inheritance is not supporting
            list.add(new Square()); // Error:  is not applicable for the arguments (Square) i.e. inheritance is not supporting
            list.add(new Rectangle()); // Error:  is not applicable for the arguments (Rectangle) i.e. 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 i.e Consumer`super`)
    * */

        public void testContraVariance(List<? super Shape> list) {
            list.add(new Shape());//compiles i.e. inheritance is supporting
            list.add(new Circle());//compiles i.e. inheritance is  supporting
            list.add(new Square());//compiles i.e. inheritance is supporting
            list.add(new Rectangle());//compiles i.e. inheritance is supporting
            Shape shape= list.get(0); // Error: Type mismatch, so list acts only as consumer
            Object object= list.get(0); // gets an object, but we don't know what kind of Object it is.

            /*You can add a Shape,Circle,Square,Rectangle to a List<? super Shape>
            * You can't get an Shape(but can get Object) and don't know what kind of Shape it is.
            */
     
        }
    }

    泛型和示例


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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是乔希·布洛克(Josh Bloch)发明的一种助记装置,用来帮助记忆制片人extends,消费者super

    This means that when a parameterized type being passed to a method will produce instances of T (they will be retrieved from it in some way), ? extends T should be used, since any instance of a subclass of T is also a T.

    When a parameterized type being passed to a method will consume instances of T (they will be passed to it to do something), ? super T should be used because an instance of T can legally be passed to any method that accepts some supertype of T. A Comparator could be used on a Collection, for example. ? extends T would not work, because a Comparator could not operate on a Collection.

    注意,一般情况下,对于某些方法的参数,应该只使用? extends T? super T。方法只应使用T作为泛型返回类型的类型参数。


    简言之,记住PEC的三个简单规则:

  • 如果需要检索的对象,请使用通配符从集合中键入T
  • 如果需要将T类型的对象放入收藏。
  • 如果您需要同时满足这两个条件,那么,不要使用任何通配符。AS很简单。

  • (添加一个答案,因为用泛型通配符的例子从来就不够多)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
           // 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);
            }
        }

    让我们假设这个层次结构:

    1
    2
    3
    4
    5
    6
    class Creature{}// X
    class Animal extends Creature{}// Y
    class Fish extends Animal{}// Z
    class Shark extends Fish{}// A
    class HammerSkark extends Shark{}// B
    class DeadHammerShark extends HammerSkark{}// C

    让我们澄清一下PE生产商的扩展:

    1
    List<? extends Shark> sharks = new ArrayList<>();

    为什么不能在此列表中添加扩展"shark"的对象?像:

    1
    sharks.add(new HammerShark());//will result in compilation error

    由于在运行时有一个可以是A、B或C的列表,所以不能在其中添加任何类型的A、B或C对象,因为最终可以使用Java中不允许的组合。实际上,编译器在compileTime中确实可以看到添加了b:

    1
    sharks.add(new HammerShark());

    …但它无法判断在运行时,您的B是否是列表类型的子类型或父类型。在运行时,列表类型可以是A、B、C类型中的任何一种。因此,您不能以在Deadhammershark列表中添加Hammerskark(超级类型)为例。

    *你会说:"好吧,但是为什么我不能在里面加上Hammerskark,因为它是最小的类型?".答:它是你所知道的最小的。购买Hammerskark也可以由其他人扩展,最终你也会遇到同样的情况。

    让我们澄清一下CS-Consumer Super:

    在相同的层次结构中,我们可以尝试:

    1
    List<? super Shark> sharks = new ArrayList<>();

    什么以及为什么可以添加到此列表中?

    1
    2
    3
    sharks.add(new Shark());
    sharks.add(new DeadHammerShark());
    sharks.add(new HammerSkark());

    您可以添加上述类型的对象,因为Shark(A、B、C)以下的任何对象都将始终是Shark(X、Y、Z)以上的任何对象的子类型。容易理解。

    不能在shark之上添加类型,因为在运行时,添加的对象的类型在层次结构中可以高于声明的列表类型(x、y、z)。这是不允许的。

    但是为什么你不能从这个列表中读?(我的意思是,您可以从中获取元素,但不能将其分配给对象o以外的任何其他对象):

    1
    2
    3
    4
    5
    Object o;
    o = sharks.get(2);// only assignment that works

    Animal s;
    s = sharks.get(2);//doen't work

    在运行时,列表类型可以是a:x、y、z……之上的任何类型。编译器可以编译您的赋值语句(看起来是正确的),但是,在运行时,s(animal)的类型在层次结构上可能比声明的列表类型(可能是生物或更高)低。这是不允许的。

    总结

    我们使用在列表中添加类型等于或小于t的对象。我们无法阅读它。我们使用从列表中读取类型等于或小于t的对象。我们不能向其中添加元素。


    记住这一点:

    Consumer eat supper(super); Producer extends his parent's factory


    协方差:接受子类型反向:接受父类型

    协变类型是只读的,而逆变类型是只读的。


    使用现实生活中的例子(经过一些简化):

  • 想象一下,一列货车和一张清单类似。
  • 如果货物的尺寸等于或小于货车=的尺寸,您可以将货物放在货车中。
  • 如果您的仓库中有足够的地方(超过货物的大小),您可以从货车上卸下货物=