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

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


当前回答

请记住:

消费者吃晚餐(超级);生产商扩大了其母公司的工厂

其他回答

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

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-Producer扩展:

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

为什么不能在此列表中添加扩展“Shark”的对象?如:

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

由于您有一个在运行时可以是a、B或C类型的列表,因此您不能在其中添加任何a、B和C类型的对象,因为您可能会得到一个在java中不允许的组合。实际上,编译器确实可以在编译时看到您添加了一个B:

sharks.add(new HammerShark());

…但它无法确定在运行时,您的B是列表类型的子类型还是超类型。在运行时,列表类型可以是A、B、C中的任何一种类型。因此,例如,您不能在DeadHammerShark列表中添加HammerSkark(超级类型)。

*你会说:“好吧,但既然它是最小的类型,为什么我不能在其中添加HammerSkark?”。答:这是你知道的最小的。但HammerSkark也可以被其他人扩展,你最终也会遇到同样的情况。

让我们来澄清一下CS-超级消费者:

在同一层次结构中,我们可以尝试以下操作:

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

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

sharks.add(new Shark());
sharks.add(new DeadHammerShark());
sharks.add(new HammerSkark());

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

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

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

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)的类型在层次结构上可以低于列表的声明类型(可以是Creature,也可以更高)。这是不允许的。

综上所述

我们使用<?super T>将类型等于或低于T的对象添加到列表中。我们无法阅读它我们使用<?扩展T>以从列表中读取类型等于或低于T的对象。我们不能向其中添加元素。

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

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

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

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

请记住:

消费者吃晚餐(超级);生产商扩大了其母公司的工厂

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

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

PECS“规则”仅确保以下内容合法:

消费者:什么?它可以合法地指代T制片人:什么?是的,它可以在法律上被T

列表中的典型配对<?扩展T>生产者,列表<?super T>使用者只是确保编译器可以强制执行标准的“is-A”继承关系规则。如果我们可以合法地这样做,那么说<T extends?>、<?extended T>(或者在Scala中更好,如您所见,它是[-T],[+T]。不幸的是,我们能做的最好的是<?super T>,<?extended T>。

当我第一次遇到这种情况并在脑海中分解时,机制是有道理的,但代码本身对我来说仍然很困惑——我一直在想“似乎边界不应该像那样颠倒”——尽管我对上面的内容很清楚——这只是为了保证遵守标准参考规则。

帮助我的是将普通作业作为类比。

考虑以下(非生产就绪)玩具代码:

// copies the elements of 'producer' into 'consumer'
static <T> void copy(List<? extends T> producer, List<? super T> consumer) {
   for(T t : producer)
       consumer.add(t);
}

用任务类比来说明这一点,对于消费者来说?通配符(未知类型)是引用-赋值的“左侧”-和<?超级T>确保了什么?T“is-A”?-T可以分配给它,因为?是T的超类型(或最多相同类型)。

对于制片人来说,他们的担忧是一样的,只是颠倒了:制片人的?通配符(未知类型)是指代-赋值的“右手边”-和<?扩展T>确保了什么?是“IS-A”T-它可以分配给T,因为?是T的子类型(或至少相同类型)。