面試官:什麼是泛型擦除、泛型上界、泛型下界、PECS原則?

文章很長,且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 博客園版 爲您奉上珍貴的學習資源 :

免費贈送 :《尼恩Java面試寶典》 持續更新+ 史上最全 + 面試必備 2000頁+ 面試必備 + 大廠必備 +漲薪必備
免費贈送 :《尼恩技術聖經+高併發系列PDF》 ,幫你 實現技術自由,完成職業升級, 薪酬猛漲!加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷1)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷2)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷3)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領

免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取


面試官:什麼是泛型擦除、泛型上界、泛型下界、PECS原則?

尼恩說在前面

在40歲老架構師 尼恩的讀者交流羣(50+)中,最近有小夥伴拿到了一線互聯網企業如阿里、滴滴、極兔、有贊、希音、百度、網易、美團的面試資格,遇到很多很重要的面試題:

問題1:什麼是PECS原則? 說說具體怎麼用?

問題2:什麼是 泛型擦除? 說說原理?

問題3:什麼是泛型上界? 什麼是泛型下界?

最近又有小夥伴在面試美團,遇到了相關的面試題。

很多小夥伴說,自己對什麼PECS 原則,可以說一臉懵逼,面試官不滿意,面試掛了。

藉着此文,尼恩給大家做一下系統化、體系化的梳理,使得大家內力猛增,展示一下雄厚的 “技術肌肉、技術實力”,讓面試官愛到 “不能自已、口水直流”,然後實現”offer直提,offer自由”。

當然,這道面試題,以及參考答案,也會收入咱們的 《尼恩Java面試寶典PDF》V162版本,供後面的小夥伴參考,提升大家的 3高 架構、設計、開發水平。

《尼恩 架構筆記》《尼恩高併發三部曲》《尼恩Java面試寶典》的PDF,請到公衆號【技術自由圈】獲取

首先,從Java的泛型說起。

本文目錄

什麼是泛型

泛型的本質是 類型參數化,解決類型爆炸的問題。

所謂泛型是指將類型參數化,以達到代碼複用提高軟件開發工作效率的一種數據類型。

比如: 如果我們的代碼中存在很多的 食物類型, 繼承關係如下

然後我們要定義一個盤子 plate,注意這個盤子除了 裝入食物food之外,還可以裝其他的比如 小玩具。

爲了裝不同類型的食物,我們需要定義不同的盤子:

(1) 裝水果的盤子  FruitPlate

(2) 裝肉的盤子  MeatPlate

(3) 裝蘋果的盤子  ApplePlate

(4) 裝香蕉的盤子  BananaPlate

.....

(N) 裝雲南蘋果的盤子  YunnanFruitPlate

這就是盤子類型的 類型爆炸。

如何解決上面的類型爆炸問題呢? 這就要用到泛型。

那麼盤子裏的東西的類型,我們就用泛型

//盤子裏的東西
private T someThing;

從這個例子看到:泛型是一種類型佔位符,或稱之爲類型參數

如何使用呢?

public static void main(String[] args) {

    //創建一個裝肉的盤子
    PlateDemo1<Meat> plateDemo1 =new PlateDemo1<>(new Pork());

    //創建一個裝水果的盤子
    PlateDemo1<Fruit> plateDemo2 =new PlateDemo1<>(new Apple());

}

所謂泛型,就是 數據類型 指定爲一個參數,在不創建新類的情況下,通過創建變量的時候去確定 數據的具體類型。

也就是說,在創建對象或者調用方法的時候才明確下具體的類型。

泛型定義:泛型類、泛型接口、泛型方法

泛型定義格式:

<類型>:指定一種類型的格式,這裏的類型可以看做是形參

<類型1,類型2...>:指定多種類型的格式,多種類型之間用逗號隔開。定義的時候是泛型形參

這個泛型形參,將來具體調用時候,需要有給定的類型,那個給定的具體的Java類型可以看出是實參。

泛型可以在類、接口、方法中使用,分別稱爲泛型類、泛型接口、泛型方法。

第一類:泛型類
定義格式:

修飾符 class 類名<類型> { }

上面的例子就是 泛型類

//盤子,可以裝 任何東西,包括 食物 其他
class PlateDemo1<T> {

    //盤子裏的東西
    private T someThing;
}

第二類:泛型方法

定義格式:

修飾符 <泛型類型> 返回值類型 方法名(類型 變量名) { }

示例代碼:

public <T> void demo(T t) { 
    ...
} 

第三類:泛型接口

定義格式:

修飾符 interface 接口名<類型> { }

示例代碼:

public interface Generic<T> { 
    void demo(T t); 
}

泛型接口的實現類

public class GenericImpl<T> implements Generic<T> { 
    public void demo(T t) { 
      ...
    } 
}

爲啥不用Object做泛型化?

沒有泛型的情況的下,好像Object也能實現簡單的 泛化。

通過定義爲類型Object的引用,來實現參數的“任意化”。

比如上面的例子的 泛型類

//盤子,可以裝 任何東西,包括 食物 其他
class PlateDemo1<T> {

    //盤子裏的東西
    private T someThing;
}

通過定義爲類型Object的引用,來實現參數的“任意化”,結果如下

//盤子,可以裝 任何東西,包括 食物 其他
class PlateDemo1   {

    //盤子裏的東西
    private Object  someThing;
}

Object實現參數的 “泛型化”、“任意化”帶來的缺點是:要做顯式的強制類型轉換。

參數類型強制轉換有一個大大降低代碼複用性和擴展性的壞處:

  • 首先,要求開發者對實際參數類型可預知。
  • 其次,不利於未來的 擴展。

泛型的兩大好處

而引入泛型後,有如下好處:

1、避免了強制類型轉換,提高代碼的複用性和擴展性

泛型中,所有的類型轉換都是自動和隱式的,不需要強制類型轉換,可以提高代碼的重用率,再加上明確的類型信息,代碼的可讀性也會更好。

2、把運行時期的問題提前到了編譯期,編譯時的類型檢查,使程序更加健壯

使用普通的Object泛化,對於強制類型轉換錯誤的情況,編譯期不會提示錯誤,在運行的時候纔出現異常,這是一個安全隱患。

泛型的好處是在編譯期檢查類型安全,並能捕捉類型不匹配的錯誤,避免運行時拋出類型轉化異常ClassCastException,將運行時錯誤提前到編譯時錯誤,消除安全隱患。

正是由於以上兩點原因,泛型得到了廣泛的應用。

比如Java中,所有的標準集合接口都是泛型化的:Collection<V>List<V>Set<V>Map<K,V>

泛型的上界/ 上界通配符(Upper Bounds Wildcards)

現在我定義一個“水果盤子”,用來裝蘋果, 邏輯上水果盤子當然可以裝蘋果。

那麼,一個“裝蘋果的盤子”,能轉換成一個“裝水果的盤子”嗎?

看下面的例子

那麼,一個“裝蘋果的盤子”,能轉換成一個“裝水果的盤子”嗎? 答案是不行的。

編譯器 的邏輯是這樣的:

  • 蘋果 is-a 水果
  • 裝蘋果的盤子 not is-a 裝水果的盤子

也就是說:就算 蘋果 is-a 水果,但容器之間是沒有繼承關係的。

怎麼辦?這裏用到了 泛型上界。 泛型上界是這麼定義的:

<?extends 基類B> 

<?extends 基類B> 表示泛型實參類型的上界是“基類B”,

換句話說,泛型實參的類型,可能是“基類B” 或者是“基類B”的子類;

修改之後的例子如下,使用 泛型上界通配符(Upper Bounds Wildcards)後,編譯器就不報錯誤了:

使用(Upper Bounds Wildcards)通配符作爲泛型實參,所定義 PlateDemo1<? extends Fruit> 引用,可以 覆蓋下圖中方框內部的所有子類的 泛型對象。

<?extends T> 表示類型的上界,參數化類型可能是T 或者是 T的子類;

PlateDemo1<? extends Fruit> 引用,可以 覆蓋下圖中方框內部的所有子類的 泛型對象,編譯器都不報錯,下面的代碼如下:

爲啥<? extends Fruit> 叫做 上界,而不叫下屆? 原因是: 這個通配符,定義了實參的類型上限 爲 Fruit,具體如下圖:

上界通配符(Upper Bounds Wildcards)的問題

上界通配符(Upper Bounds Wildcards)的作用,實現了 子類泛型對象 到 父類Java泛型對象之間的引用轉換。

但是,這樣的引用轉換也有一定的副作用。

具體如下:

通過例子可以看到:

(1)往基類盤子,set( ) 任何對象,都 失效了

(2)從基類盤子,get ( ) 對象的引用,返回 類型是上界對象, 這個還是 可以的

簡單來說: 上界<? extends T>不能往裏存,只能往外取

所以,上界通配符(Upper Bounds Wildcards)什麼時候用,什麼時候不用呢:

(1)當從集合中獲取元素進行操作的時候用,可以用當前元素的類型接收,也可以用當前元素的父類型接收。

(2)往集合中添加元素時,不能用上界通配符(Upper Bounds Wildcards)。

泛型的下界/ 下界通配符(Lower Bounds Wildcards)

往集合中添加元素時,不能用上界通配符(Upper Bounds Wildcards)。

怎麼辦呢? Java也提供了一種通配符,叫做 泛型的下界/ 下界通配符(Lower Bounds Wildcards)。

泛型上界是這麼定義的:

<?super 子類C> 

<?super 子類C> 表示泛型實參類型的下界是“子類C”,

<? super T> 表示 T是類型下邊界,參數化類型是此T類型的超類型,直至object;

尼恩認爲下面的這一張圖,下界的感覺更加強烈一些:

下界/ 下界通配符(Lower Bounds Wildcards)的問題

下界/ 下界通配符(Lower Bounds Wildcards) 作用,實現了 復類泛型對象 到 子類Java泛型對象之間的引用轉換。

但是,這樣的引用轉換也有一定的副作用。

具體如下:

通過例子可以看到:

(1)往基類盤子,set( ) 任何子類對象,都是OK的

(2)從基類盤子,get ( ) 對象的引用是編譯錯誤的,除非是Object類型

簡單來說:下界<? super T>可以往裏存,但不能向外取,要取只能取Object對象

所以,下界/ 下界通配符(Lower Bounds Wildcards)什麼時候用,什麼時候不用呢:

(1)當往集合中添加元素時候用,既可以添加T類型對象,又可以添加T的子類型對象

(2)當從集合get ( ) 對象的引用時,不能用上界通配符(Upper Bounds Wildcards)。除非get 的是Object類型

PECS原則

PECS原則的全稱是Producer Extends Consumer Super,很多小夥伴從沒聽說過,面試的時候,只要面試官一問,大部分都是一臉懵逼。

什麼是PECS(Producer Extends Consumer Super)原則?PECS原則全稱"Producer Extends, Consumer Super",即上界生產,下界消費。

  • Producer Extends 上界生產,就是 生產者使用 “? extends T”通配符。

  • Consumer Super 下界消費,就是消費者使用 “? super T”通配符

最終PECS (Producer Extends Consumer Super ) 原則

  • 頻繁往外讀取內容的,適合用上界Extends。
  • 經常往裏插入的,適合用下界Super。

在阿里編程規範中,就有這麼一條:

【強制】泛型通配符<? extends T>來接收返回的數據,此寫法的泛型集合不能使用 add 方法, 而<? super T>不能使用 get 方法,兩者在接口調用賦值的場景中容易出錯。

1. Producer Extends 上界生產

Producer Extends 上界生產,就是 生產者使用 “? extends T”通配符。

以“? extends T”聲明的集合,不能往此集合中添加元素,所以它也只能作爲生產者,如下:

所以,使用 “? extends T” 上界,能輕鬆地成爲 producer 生產者,完成

  • 讀取元素
  • 迭代元素

這就是 Producer Extends 上界生產,就是 生產者使用 “? extends T”通配符。

2. Consumer Super 下界消費,就是消費者使用 “? super T”通配符

在通配符的表達式中,只有“? super T”能添加元素,所以它能作爲消費者(消費其他通配符集合)。

當然,針對採用“? super T”通配符的集合,對其遍歷時需要多一次轉型。

總之 PECS就是:

1、頻繁往外讀取內容的,適合用上界Extends。

2、經常往裏插入的,適合用下界Super

明白了泛型、泛型的上界,泛型的下屆之後, 尼恩帶大家來回答這個面試的核心問題: 什麼是泛型的擦除。

泛型的類型擦除

前面講到,泛型的本質是 類型參數化,解決類型爆炸的問題。比如: 如果我們的代碼中存在很多的 食物類型, 繼承關係如下

沒有泛型,爲了實現去裝不同類型的食物,我們需要定義不同的盤子:

(1) 裝水果的盤子  FruitPlate

(2) 裝肉的盤子  MeatPlate

(3) 裝蘋果的盤子  ApplePlate

(4) 裝香蕉的盤子  BananaPlate

.....

(N) 裝雲南蘋果的盤子  YunnanFruitPlate

如何解決上面的類型爆炸問題呢? 這就要用到泛型。

而使用泛型,我們定義一個就可以了:

//盤子,可以裝 任何東西,包括 食物 其他
class PlateDemo1<T> {

    //盤子裏的東西
    private T someThing;

    public PlateDemo1(T t) {
        someThing = t;
    }
    ....
}

這樣,就避免 了 盤子類型的 類型爆炸。尤其在Java中的集合類,如果不用泛型,不知道要定義多少的具體集合類。

那麼 Java中的泛型,有一個 類型擦除 的特點:

  • java的泛型,只在編譯期有效。

  • 編譯之後的字節碼,已經抹除了泛型信息。

所謂的類型擦除(type erasure),指的是泛型只在編譯時起作用,在進入JVM之前,泛型會被擦除掉,根據泛型定義的形式而被替換爲相應的類型。這也說明了Java的泛型其實是僞泛型。

類型擦除簡單來說,泛型類型在邏輯上可以看成是多個不同的類型,實際上都是相同類型。

比如:

Food food = new Fruit(); // 沒問題
ArrayList<Food> list= new ArrayList<Fruit>(); // 報錯

或者說下面的ArrayList ,在邏輯上看,可以看成是多個不同的類型,實際上都是相同類型

ArrayList<Food> list1
ArrayList<Fruit> list2
ArrayList<Apple> list3
.....

泛型類型在邏輯上可以看成是多個不同的類型,但實際上都是相同的類型。

看下面的例子

類型參數在運行中並不存在,這意味着:

  • 運行期間,泛型不會添加任何的類型信息;
  • 不能依靠泛型參數,進行類型轉換。

Java泛型的實現是靠類型擦除技術實現的,類型擦除是在編譯期完成的,泛型擦除怎麼做呢?

  • 在編譯期,編譯器會將泛型的類型參數都擦除成它指定的原始限定類型
  • 如果沒有指定的原始限定類型則擦除爲Object類型,之後在獲取的時候再強制類型轉換爲對應的類型,
  • 因此生成的Java字節碼中是不包含泛型中的類型信息的,即運行期間並沒有泛型的任何信息。

無界泛型擦除

當泛型類型被聲明爲一個具體的泛型標識,或一個無界通配符時,泛型類型將會被替代爲Object

這也比較容易理解,如 List<?>,PlateDemo1<?>, 當獲取元素的時,因爲不能夠確定具體的類型,所以只能使用Object來接收,

在擦除的時候也是一樣的道理,無法確定具體類型,所以擦除泛型時會將其替換爲Object類型,如:

上界擦除

當泛型類型被聲明爲一個上界通配符時,泛型類型將會被替代爲相應上界的類型。

主要,這裏的上界,指的是用於類型定義場景裏邊的上界:

而不是變量定義場景裏邊用到到泛型上界,如下:

 List<? extends Fruit> producer =...;

用泛型上界定義class的時候,指的是用於類型定義,泛型類型將會被替代爲相應上界的類型。

下界擦除

下界通配符的擦除,同無界通配符

下屆只能定義引用的時候用,在定義類型的時候用不了,所以下界擦除只能替換爲Object

下界擦除只能替換爲Object

說在最後:老馬識途,有問題找老架構求助

以上的內容,如果大家能對答如流,如數家珍,基本上 面試官會被你 震驚到、吸引到。

最終,讓面試官愛到 “不能自已、口水直流”。 offer, 也就來了。

其實, “offer自由” 不難實現, 前段時間一個跟尼恩捲了2年的武漢小夥,9年經驗, 在年底大裁員的極度嚴寒/痛苦被裁的背景下, offer拿到手軟, 實現真正的 “offer自由” 。

在面試之前,建議大家系統化的刷一波 5000頁《尼恩Java面試寶典PDF》,裏邊有大量的大廠真題、面試難題、架構難題。很多小夥伴刷完後, 吊打面試官, 大廠橫着走

在刷題過程中,如果有啥問題,大家可以來 找 40歲老架構師尼恩交流。

尼恩一直深耕技術,不是在研究技術,就是在研究技術的路上,加尼恩微信之後不一定立馬通過, 但是,最多1-2小時就會審覈的。

深研技術,遠離浮躁。作爲資深技術人,尼恩實在太忙了.....

特別要說的是:很多小夥伴簡歷投出去後如泥牛入海、不冒一泡、沒有面試機會。

遇到這種難題,可以找尼恩來改簡歷、做幫扶。

另外,遇到架構升級、晉升受阻、職業打擊等職業難題,也可以找尼恩取經, 可以省去太多的折騰,省去太多的彎路。

尼恩已經指導了大量的小夥伴上岸,前段時間指導一個40歲+被裁小夥伴上岸,拿到了一個年薪100W的offer。

技術自由的實現路徑:

實現你的 架構自由:

喫透8圖1模板,人人可以做架構

10Wqps評論中臺,如何架構?B站是這麼做的!!!

阿里二面:千萬級、億級數據,如何性能優化? 教科書級 答案來了

峯值21WQps、億級DAU,小遊戲《羊了個羊》是怎麼架構的?

100億級訂單怎麼調度,來一個大廠的極品方案

2個大廠 100億級 超大流量 紅包 架構方案

… 更多架構文章,正在添加中

實現你的 響應式 自由:

響應式聖經:10W字,實現Spring響應式編程自由

這是老版本 《Flux、Mono、Reactor 實戰(史上最全)

實現你的 spring cloud 自由:

Spring cloud Alibaba 學習聖經》 PDF

分庫分表 Sharding-JDBC 底層原理、核心實戰(史上最全)

一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之間混亂關係(史上最全)

實現你的 linux 自由:

Linux命令大全:2W多字,一次實現Linux自由

實現你的 網絡 自由:

TCP協議詳解 (史上最全)

網絡三張表:ARP表, MAC表, 路由表,實現你的網絡自由!!

實現你的 分佈式鎖 自由:

Redis分佈式鎖(圖解 - 秒懂 - 史上最全)

Zookeeper 分佈式鎖 - 圖解 - 秒懂

實現你的 王者組件 自由:

隊列之王: Disruptor 原理、架構、源碼 一文穿透

緩存之王:Caffeine 源碼、架構、原理(史上最全,10W字 超級長文)

緩存之王:Caffeine 的使用(史上最全)

Java Agent 探針、字節碼增強 ByteBuddy(史上最全)

實現你的 面試題 自由:

4800頁《尼恩Java面試寶典 》 40個專題

免費獲取11個技術聖經PDF:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章