面試常問的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新增了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的圖。

我們返回頭來再看<? 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有更深入的了解,可以深入了解一下函數編程相關的這四個介面。哪怕你只是看一下它的定義,也會有一種原來如此的感覺。

Exit mobile version