我在阅读泛型时遇到了PECS(Producer extends和Consumer super的缩写)。

有人能向我解释一下如何使用PECS来解决extends和super之间的混淆吗?


当前回答

计算机科学背后的原理叫做

协方差:?扩展MyClass,矛盾:?超级MyClass和不变性/非方差:MyClass

下图应解释该概念。图片提供:Andrey Tyukin

其他回答

计算机科学背后的原理叫做

协方差:?扩展MyClass,矛盾:?超级MyClass和不变性/非方差:MyClass

下图应解释该概念。图片提供:Andrey Tyukin

(添加答案,因为使用泛型通配符的示例永远不够)

       // 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);
        }
    }

这是我认为extends与super最清晰、最简单的方式:

扩展用于读取super是用来写作的

我发现“PECS”是一种不明显的方式来思考谁是“生产者”,谁是“消费者”。“PECS”是从数据集合本身的角度定义的——如果对象正在被写入到集合中,则集合“消耗”(它消耗来自调用代码的对象),如果对象正在从集合中读取,则它“产生”(它向某些调用代码产生对象)。这与其他所有事物的命名方式相反。标准JavaAPI是从调用代码的角度命名的,而不是从集合本身命名的。例如,java.util.List的以集合为中心的视图应该有一个名为“receive()”的方法,而不是“add()”——毕竟,调用代码添加了元素,但列表本身接收了元素。

我认为从与集合交互的代码的角度来思考事情更直观、更自然、更一致——代码是“从集合中读取”还是“写入”集合?之后,向集合写入的任何代码都将是“生产者”,从集合读取的任何代码将是“消费者”。

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。信用

PECS(生产者延伸和消费者超级)

助记符→ 获取(扩展)和放置(超级)原则。

该原则规定:仅从结构中获取值时,请使用扩展通配符。仅将值放入结构中时,请使用超级通配符。当你得到和得到时,不要使用通配符。

Java示例:

class Super {
        Number testCoVariance() {
            return null;
        }
        void testContraVariance(Number parameter) {
        } 
    }
    
    class Sub extends Super {
        @Override
        Integer testCoVariance() {
            return null;
        } //compiles successfully i.e. return type is don't care(Integer is subtype of Number)
        @Override
        void testContraVariance(Integer parameter) {
        } //doesn't support even though Integer is subtype of Number
    }

Liskov替换原则(LSP)指出,“程序中的对象应该用其子类型的实例替换,而不改变程序的正确性”。

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

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

协方差和反方差

只读数据类型(源)可以是协变的;只写数据类型(汇)可以是相反的。充当源和汇的可变数据类型应该是不变的。

要说明这种一般现象,请考虑数组类型。对于Animal类型,我们可以使用Animal[]类型

协变:猫[]是动物[];反义词:动物[]是猫[];不变量:动物[]不是猫[],猫[]不是动物[]。

Java示例:

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  

更多示例

图像src

有界(即朝向某处)通配符:有三种不同的通配符:

差异/非差异:?或扩展对象-无边界通配符。它代表所有类型的家庭。当你得到和投入时使用。共方差:?扩展T(T子体的统治)-带有上限的通配符。T是继承层次结构中最上层的类。仅从结构中获取值时,请使用扩展通配符。抵销方差:?超级T(T祖先的统治)-带有下限的通配符。T是继承层次结构中最底层的类。当只将值放入结构中时,请使用超级通配符。

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

 import java.util.ArrayList;
import java.util.List;

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 {

    public static void main(String[] args) {
        //? extends Shape i.e. can use any sub type of Shape, here Shape is Upper Bound in inheritance hierarchy
        List<? extends Shape> intList5 = new ArrayList<Shape>();
        List<? extends Shape> intList6 = new ArrayList<Cricle>();
        List<? extends Shape> intList7 = new ArrayList<Rectangle>();
        List<? extends Shape> intList9 = new ArrayList<Object>();//ERROR.


        //? super Shape i.e. can use any super type of Shape, here Shape is Lower Bound in inheritance hierarchy
        List<? super Shape> inList5 = new ArrayList<Shape>();
        List<? super Shape> inList6 = new ArrayList<Object>();
        List<? super Shape> inList7 = new ArrayList<Circle>(); //ERROR.

        //-----------------------------------------------------------
        Circle circle = new Circle();
        Shape shape = circle; // OK. Circle IS-A Shape

        List<Circle> circles = new ArrayList<>();
        List<Shape> shapes = circles; // ERROR. List<Circle> is not subtype of List<Shape> even when Circle IS-A Shape

        List<? extends Circle> circles2 = new ArrayList<>();
        List<? extends Shape> shapes2 = circles2; // OK. List<? extends Circle> is subtype of List<? extends Shape>


        //-----------------------------------------------------------
        Shape shape2 = new Shape();
        Circle circle2= (Circle) shape2; // OK. with type casting

        List<Shape> shapes3 = new ArrayList<>();
        List<Circle> circles3 = shapes3; //ERROR. List<Circle> is not subtype of  List<Shape> even Circle is subetype of Shape

        List<? super Shape> shapes4 = new ArrayList<>();
        List<? super Circle> circles4 = shapes4; //OK.
    }

    
    
    /*
     * Example for an upper bound wildcard (Get values i.e Producer `extends`)
     *
     * */
    public void testCoVariance(List<? extends Shape> list) {
        list.add(new Object());//ERROR
        list.add(new Shape()); //ERROR
        list.add(new Circle()); // ERROR
        list.add(new Square()); // ERROR
        list.add(new Rectangle()); // ERROR
        Shape shape= list.get(0);//OK 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 Object());//ERROR
        list.add(new Shape());//OK
        list.add(new Circle());//OK
        list.add(new Square());//OK
        list.add(new Rectangle());//OK
        Shape shape= list.get(0); // ERROR. Type mismatch, so list acts only as consumer
        Object object= list.get(0); //OK 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.
         */
    }
}

泛型和示例

协方差和反方差根据类型确定兼容性。在任何一种情况下,方差都是有向关系。协方差可以翻译为“同一方向不同”或不同,而反方差意味着“相反方向不同”,或相反。协变型和逆变型并不相同,但它们之间存在相关性。这些名称暗示了相关性的方向。

https://stackoverflow.com/a/54576828/1697099https://stackoverflow.com/a/64888058/1697099

协方差:接受子类型(只读,即Producer)相反:接受超类型(仅写,即Consumer)