我在阅读泛型时遇到了PECS(Producer extends和Consumer super的缩写)。
有人能向我解释一下如何使用PECS来解决extends和super之间的混淆吗?
我在阅读泛型时遇到了PECS(Producer extends和Consumer super的缩写)。
有人能向我解释一下如何使用PECS来解决extends和super之间的混淆吗?
当前回答
计算机科学背后的原理叫做
协方差:?扩展MyClass,矛盾:?超级MyClass和不变性/非方差:MyClass
下图应解释该概念。图片提供:Andrey Tyukin
其他回答
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:生产者延伸和消费者超级
理解的前提条件:
泛型和泛型通配符多态性、亚型和超型
假设我们有一个采用泛型类型参数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);
}
让我们尝试可视化这个概念。
<? 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的小盒子里。
如果有人读到这篇文章,我希望它能帮助你更好地理解“生产者延伸,消费者超级。”
快乐编码!:)
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);
tl;博士:“PECS”是从藏品的角度来看的。如果您只是从泛型集合中提取项,那么它是一个生产者,您应该使用extends;如果你只是在填充物品,那么它是一个消费者,你应该使用super。如果同时使用同一集合,则不应使用extends或super。
假设您有一个方法,它以一个集合作为参数,但您希望它比只接受集合<Thing>更灵活。
案例1:你想通过收集并处理每个项目。那么列表是一个生产者,因此您应该使用集合<?扩展Thing>。
理由是集合<?extendsThing>可以保存Thing的任何子类型,因此当您执行操作时,每个元素都将表现为Thing。(实际上不能向集合中添加任何内容(null除外)<?扩展Thing>,因为您无法在运行时知道集合持有Thing的哪个特定子类型。)
案例2:您想要向集合中添加内容。那么列表是消费者,因此您应该使用集合<?超级事物>。
这里的理由是,不同于集合<?扩展Thing>,Collection<?super Thing>可以始终保存Thing,无论实际参数化类型是什么。在这里,只要它允许添加Thing,您就不在乎列表中已经存在什么;这是什么?super Thing保证。