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

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


当前回答

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

其他回答

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

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

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

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

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

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

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

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

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

使用现实生活中的示例(有一些简化):

想象一列有货车的货运火车,类似于一个列表。如果货物的尺寸与货车相同或更小,您可以将货物放入货车=<?超级货车尺寸>如果您的仓库有足够的空间(大于货物的大小),您可以从货运车上卸载货物=<?扩展DepotSize>

正如我在回答另一个问题时所解释的,PECS是乔什·布洛克(Josh Bloch)为帮助记住Producer extends和Consumer super而创建的助记符设备。

这意味着当传递给方法的参数化类型将生成T的实例(它们将以某种方式从中检索)时?应该使用extends T,因为T的子类的任何实例也是T。当传递给方法的参数化类型将消耗T的实例(它们将传递给它以执行某些操作)时?应该使用super T,因为T的实例可以合法地传递给任何接受某个T超类型的方法。例如,可以在Collection<Integer>上使用Comparator<Number>?extends T无法工作,因为Comparator<Integer>无法对Collection<Number>进行操作。

请注意,通常您只应使用?延伸T和?super T表示某些方法的参数。方法应该只使用T作为泛型返回类型的类型参数。