面試常問的PECS原則,到底是什麼鬼?

NO IMAGE

面試常問的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新增了staticdefault方法,我們不去過多關注。你會發現,裡面有<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的圖。

面試常問的PECS原則,到底是什麼鬼?

我們返回頭來再看<? extends T>,只要後面的new,聲明的是T的子類或者T本身,那麼都是沒錯的。反之,如果是它的父類,則報錯。這很好理解,後半部分的實例,一定要能夠全面覆蓋前面的聲明。這也就是Producer-Extends,它可以對外提供對象(難以理解的概念)。

接下來我們看一下<? super T>。只要是T的父類或者T本身,都沒有什麼問題,甚至可以是Object。比如,下面的代碼就不會報錯。

Example<? super C> testCO = new Example<Object>();

根據字面意思,Consumer-super也比較晦澀,如果設計的類是消費者,那應該用super關鍵字為此類型指定一個子類。

面試常問的PECS原則,到底是什麼鬼?

這張圖只畫了聲明部分的原則。為了配合上面這張圖,進行更精細的理解,我們創建一個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,歡迎添加好友,​進一步交流。​

面試常問的PECS原則,到底是什麼鬼?

相關文章

JS基本類型、引用類型梳理

PostGIS_小白筆記_行政區劃邊界

基於函數計算+TensorFlow的ServerlessAI推理

npx:npm包執行工具