面试常问的PECS原则,到底是什么鬼?
原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。
温馨提示:泛型相关。以下内容请在安静的场所、充足的时间下查看,因为它非常的绕,容易把人绕晕。
PECS的全程是Producer Extends Consumer Super
,第一次听说,我一脸懵逼。但看到jdk中越来越多的泛型代码,我决定去了解一下。
java的泛型,只在编译期有效。也就是说,编译之后的字节码,已经抹除了泛型信息。
其实,对于常年接触业务代码的同学来说,泛型用的并不是特别多。当你使用设计模式设计代码,或者在设计一些比较底层的框架时,肯定会碰到这个问题。
一个例子
泛型该怎么写?我们首先看一下jdk中的一些例子。
java.util.function.Consumer
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
java8的interface新增了static
和default
方法,我们不去过多关注。你会发现,里面有<T,R>
字样,<? super V, ? extends T>
字样等。
那什么时候该用super,什么时候该用extends?这就是PECS原则。
为了解释这个原理,我们创建三个类。
A,B,C。
其中。A extends B,B extends C。
static class A extends B{}
static class B extends C{}
static class C {}
然后,我们使用测试类测试一下。
static class Example<T>{
}
public static void main(String[] args) {
{
Example<? extends A> testAA = new Example<A>();
Example<? extends A> testAB = new Example<B>();//报错
Example<? extends A> testAC = new Example<C>();//报错
Example<? extends B> testBA = new Example<A>();
Example<? extends B> testBC = new Example<C>();//报错
Example<? extends C> testCA = new Example<A>();
Example<? extends C> testCB = new Example<B>();
}
{
Example<? super A> testAA = new Example<A>();
Example<? super A> testAB = new Example<B>();
Example<? super A> testAC = new Example<C>();
Example<? super B> testBA = new Example<A>();//报错
Example<? super B> testBC = new Example<C>();
Example<? super C> testCA = new Example<A>();//报错
Example<? super C> testCB = new Example<B>();//报错
}
}
为了更直观一些,我们截个idea的图。
我们返回头来再看<? extends T>
,只要后面的new,声明的是T的子类或者T本身,那么都是没错的。反之,如果是它的父类,则报错。这很好理解,后半部分的实例,一定要能够全面覆盖前面的声明。这也就是Producer-Extends,它可以对外提供对象(难以理解的概念)。
接下来我们看一下<? super T>
。只要是T的父类或者T本身,都没有什么问题,甚至可以是Object。比如,下面的代码就不会报错。
Example<? super C> testCO = new Example<Object>();
根据字面意思,Consumer-super也比较晦涩,如果设计的类是消费者,那应该用super关键字为此类型指定一个子类。
这张图只画了声明部分的原则。为了配合上面这张图,进行更精细的理解,我们创建一个7层的继承关系。
static class Parent1{}
static class Parent2 extends Parent1{}
static class Parent3 extends Parent2{}
static class T extends Parent3{}
static class Child1 extends T{}
static class Child2 extends Child1{}
static class Child3 extends Child2{}
同时,我们创建两个集合容器进行验证。
List<? extends T> extendsT = new ArrayList<>();
List<? super T > superT = new ArrayList<>();
以下代码运行都是没有问题的。
List<? super T > superT = new ArrayList<>();
superT.add(new T());
superT.add(new Child1());
superT.add(new Child2());
superT.add(new Child3());
我们把代码分成两部分,一部分是泛型集合的声明部分。一部分是实例的初始化部分。可以看到,? super T界定了最小子类是T,则声明部分的最小类就是T,ArrayList
后面的<>
,可以是T的任何父类。但是,当向里面添加元素时,初始化的却是T的子类
。
再来看extendsT
。当我们往里添加数据的时候,无一例外的报错了。
extendsT.add(new T());
extendsT.add(new Child1());
extendsT.add(new Parent1());
extendsT.add(new Parent2());
extendsT.add(new Object());
那是因为,extendsT中存放的其实是T的一种子类(现象),如果我们去添加元素,其实不知道到底应该添加T的哪个子类,这个时候,在进行强转的时候,肯定会出错。但是如果是从集合中将元素取出来,我们则可以知道取出来的元素肯定是T类型(全是它的子类)。
接下来,我们再强行分析一下 ? super T
。superT中,因为存
的都是类型T的父类(容器),所以如果去添加T类或者T的子类(操作),肯定没什么问题。但是如果将元素取出来,则不知道到底是什么类型,所以superT可以添加元素但是没法取出来。
按照我们以往的经验,extendsT只出不进,属于生产者一类;superT只进不出,属于消费者。这也就有了我们上面所提到的“Producer Extends Consumer Super”,也就是PECS原则。
这个过程可真是绕,我认为这是定义非常失败的一个名词。
End
现在,再来看我们文章头部jdk的类Consumer,是不是有了新的理解?其实,这个函数是和函数编程相关的。java8的四个核心函数接口有:Function、Consumer、Supplier、Predicate。
Function<T, R>
T:入参类型,R:出参类型。Consumer<T>
T:入参类型;没有出参。
Supplier<T>
T:出参类型;没有入参。
Predicate<T>
T:入参类型;出参类型Boolean。
想要对PECS有更深入的了解,可以深入了解一下函数编程相关的这四个接口。哪怕你只是看一下它的定义,也会有一种原来如此
的感觉。
作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流。
近期热门文章
《996的乐趣,你是无法想象的》
魔幻现实主义,关爱神经衰弱
《一切荒诞的傲慢,皆来源于认知》
不要被标题给骗了,画面感十足的消遣文章
《必看!java后端,亮剑诛仙》
后端技术索引,中肯火爆。全网转载上百次。
《学完这100多技术,能当架构师么?(非广告)》
精准点评100多框架,帮你选型