从编译器的角度理解 PECS 原则

要站在编译器的角度理解 Java 泛型的 PECS 原则。

interface Fruit {
}

class Apple implements Fruit {
}

class Orange implements Fruit {
}

Producer Extends

public Fruit getFirstFromList(List<? extends Fruit> list) {
    return list.get(0);
}

方法本身作为一个生产者,传入 list 生产 Fruit,编译器须明确地知道 list 里拿到的一定是 Fruit 类型对象,所以要求 list 的泛型是 Fruit 类型,里面的对象一定是 Fruit 实现类对象。

Consumer Super

反过来,如果方法作为一个消费者,作用是想往 list 里面添加元素,那么 extends 将报错:

image-20191028011959400

站在编译器的角度上思考,你虽然告诉我 list 里的对象是 Fruit 类型的,但是 list 本身也可以是 new ArrayList<Orange>() 这样的明确泛型啊,我不能确信 fruit 能不能放进去。

这么一想,你就能理解编译器了。

解决方式是使用 super,告诉编译器我这个 list 运行时动态绑定的一定是 Fruit 或其父类类型,比如 new ArrayList<Fruit>() 或者 new ArrayList<Object>(),此时任何 Fruit 肯定都能放进去,所以编译器就不会报错了。

public void addToList(List<? super Fruit> list, Fruit fruit) {
    list.add(fruit);
}

例子

关于“PECS 原则”的一个比较好的例子是 java.util.Collections 工具类的 copy() 方法:

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
    if (srcSize > dest.size())
        throw new IndexOutOfBoundsException("Source does not fit in dest");

    if (srcSize < COPY_THRESHOLD ||
        (src instanceof RandomAccess && dest instanceof RandomAccess)) {
        for (int i=0; i<srcSize; i++)
            dest.set(i, src.get(i));
    } else {
        ListIterator<? super T> di=dest.listIterator();
        ListIterator<? extends T> si=src.listIterator();
        for (int i=0; i<srcSize; i++) {
            di.next();
            di.set(si.next());
        }
    }
}

可以看到,copy 即是消费者(从 src 中消费),也是生产者(向 dest 中生产),相当于保证了 src 中拿出来的项都是 T 类型,而 dest 自身再具体,也只能具体到 T 粒度,不可能是 T 更进一步扩展的子类。