面試常問的PECS原則,到底是什麼鬼?
- 2019 年 12 月 17 日
- 筆記
原創:小姐姐味道(微信公眾號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有更深入的了解,可以深入了解一下函數編程相關的這四個介面。哪怕你只是看一下它的定義,也會有一種原來如此
的感覺。