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

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


当前回答

简而言之,要记住PECS的三个简单规则:

使用<?如果需要检索对象集合中的类型T。使用<?如果需要将T类型的对象放入一个集合。如果您需要同时满足这两个条件,那么不要使用通配符。像就这么简单。

其他回答

PECS:生产者延伸和消费者超级

理解的前提条件:

泛型和泛型通配符多态性、亚型和超型


假设我们有一个采用泛型类型参数T的类型,例如List<T>。当我们编写代码时,还允许泛型类型参数T的子类型或超类型可能是有益的。这放松了对API用户的限制,可以使代码更加灵活。

让我们先看看放松这些限制会带来什么。假设我们有以下3个类:

class BaseAnimal{};

class Animal extends BaseAnimal{};

class Duck extends Animal{};

我们正在构建一个公共方法,该方法采用列表<Animal>

如果我们使用超级列表<?superAnimal>而不是List<Animal>,我们现在可以传递更多的列表来满足我们方法的要求。我们现在可以传入List<Animal>或List<BaseAnimal>甚至List<Object>如果我们使用扩展列表<?扩展Animal>而不是List<Animal>,我们现在可以传递更多的列表来满足我们方法的要求。我们现在可以传入List<Animal>或List<Duck>

然而,这存在以下两个限制:

如果我们使用像List<?super Animal>我们不知道List<t>的确切类型。它可能是List<Animal>或List<BaseAnimal>或List<Object>的列表。我们无从得知。这意味着我们永远无法从列表中获取值,因为我们不知道该类型是什么。但是,我们可以将任何Animal数据类型或将其扩展到列表中。因为我们只能将数据放入列表,所以它被称为数据消费者。如果我们使用扩展列表<?扩展Animal>而不是List<Animal>。我们也不知道确切的类型是什么。它可以是List<Animal>或List<Duck>。我们现在不能在列表中添加一些东西,因为我们永远无法确定是什么类型。但是我们可以删除一些东西,原因是我们始终知道列表中的任何东西都是Animal的子类型。因为我们只能从列表中提取数据,所以它被称为数据的生产者。

这里有一个简单的程序来说明类型限制的放松:

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

public class Generics {
    public static void main(String[] args) {

        Generics generics = new Generics();

        generics.producerExtends(new ArrayList<Duck>());
        generics.producerExtends(new ArrayList<Animal>());

        generics.consumerSuper(new ArrayList<Object>());
        generics.consumerSuper(new ArrayList<Animal>());

    }

    //  ? extends T   is an upper bound
    public void producerExtends (List<? extends Animal> list) {

        // Following are illegal since we never know exactly what type the list will be
        // list.add(new Duck());
        // list.add(new Animal());
        
        // We can read from it since we are always getting an Animal or subclass from it
        // However we can read them as an animal type, so this compiles fine
        if (list.size() > 0) {
            Animal animal = list.get(0);
        }
    }

    // ? extends T   is a lower bound
    public void consumerSuper (List<? super Animal> list) {
        // It will be either a list of Animal or a superclass of it
        // Therefore we can add any type which extends animals
        list.add(new Duck());
        list.add(new Animal());

        // Compiler won't allow this it could potentially be a super type of Animal
        // Animal animal = list.get(0);
    }

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

[协方差和反方差]

让我们来看看示例

public class A { }
//B is A
public class B extends A { }
//C is A
public class C extends A { }

泛型允许您以安全的方式动态处理类型

//ListA
List<A> listA = new ArrayList<A>();

//add
listA.add(new A());
listA.add(new B());
listA.add(new C());

//get
A a0 = listA.get(0);
A a1 = listA.get(1);
A a2 = listA.get(2);
//ListB
List<B> listB = new ArrayList<B>();

//add
listB.add(new B());

//get
B b0 = listB.get(0);

问题

由于Java的Collection是一种引用类型,因此我们面临以下问题:

问题#1

//not compiled
//danger of **adding** non-B objects using listA reference
listA = listB;

*Swift的泛型没有这样的问题,因为Collection是Value类型[About],因此创建了一个新的集合

问题#2

//not compiled
//danger of **getting** non-B objects using listB reference
listB = listA;

解决方案-通用通配符

通配符是引用类型功能,不能直接实例化

解决方案#1<? super A>又称下界,又称反方差,又称消费者保证它由A和所有超类操作,这就是为什么添加

List<? super A> listSuperA;
listSuperA = listA;
listSuperA = new ArrayList<Object>();

//add
listSuperA.add(new A());
listSuperA.add(new B());

//get
Object o0 = listSuperA.get(0);

解决方案#2

<? 扩展A>aka上界aka协方差aka生产者保证它由A和所有子类操作,这就是为什么它是安全的

List<? extends A> listExtendsA;
listExtendsA = listA;
listExtendsA = listB;

//get
A a0 = listExtendsA.get(0);

让我们尝试可视化这个概念。

<? super SomeType>是一个“undefined(yet)”类型,但该未定义类型应该是“SomeType”类的超类。

<?扩展SomeType>。它是一个应该扩展“SomeType”类的类型(它应该是“SomeType”类的子类)。

如果我们在Venn图中考虑“类继承”的概念,示例如下:

哺乳动物类扩展了动物类(动物类是哺乳动物类的超类)。

猫/狗类扩展了哺乳动物类(哺乳动物类是猫/狗的超类)。

然后,让我们将上图中的“圆圈”视为具有物理体积的“盒子”。

你不能把更大的盒子放进更小的盒子里。

你只能把一个小盒子放进一个大盒子里。

当你说<?super SomeType>,你想描述一个与“SomeType”框大小相同或更大的“框”。

如果你说<?扩展SomeType>,那么您需要描述一个与SomeType框大小相同或更小的“框”。

那么PECS到底是什么呢?

“生产者”的一个例子是我们只能从中读取的列表。

“消费者”的一个例子是我们只写入的列表。

请记住:

我们从“制片人”那里“阅读”,然后把这些东西带到我们自己的盒子里。我们把自己的盒子“写”成“消费者”。

因此,我们需要从“制片人”那里读到一些东西,并将其放入我们的“盒子”中。这意味着,从生产商那里拿走的任何盒子都不应该比我们的“盒子”大。这就是《制作人延伸》的原因

“延伸”是指一个较小的方框(上图中的较小圆圈)。制片人的盒子应该比我们自己的盒子小,因为我们要从制片人那里拿走这些盒子,然后把它们放进自己的盒子里。我们不能放比我们的箱子更大的东西!

此外,我们需要将自己的“盒子”写入“消费者”。这意味着消费者的箱子不应小于我们自己的箱子。这就是为什么“超级消费者”

“超级”是指一个更大的盒子(上图中的大圆圈)。如果我们想把自己的盒子放进消费者手中,消费者的盒子应该比我们的盒子大!

现在我们很容易理解这个例子:

public class Collections { 
  public static <T> void copy(List<? super T> dest, List<? extends T> src) {
      for (int i = 0; i < src.size(); i++) 
        dest.set(i, src.get(i)); 
  } 
}

在上面的示例中,我们希望从src中读取(获取)一些内容,并将其写入(放入)dest。因此src是一个“Producer”,它的“box”应该比一些类型T小(更具体)。

反之亦然,dest是一个“消费者”,它的“盒子”应该比某些类型的T更大(更一般)。

如果src的“盒子”比dest的大,我们就不能把这些大盒子放进dest的小盒子里。

如果有人读到这篇文章,我希望它能帮助你更好地理解“生产者延伸,消费者超级。”

快乐编码!:)

协方差:接受子类型相反:接受超类型

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

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

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