我在阅读泛型时遇到了PECS(Producer extends和Consumer super的缩写)。
有人能向我解释一下如何使用PECS来解决extends和super之间的混淆吗?
我在阅读泛型时遇到了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);
}
}