文章很長,且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 博客園版 爲您奉上珍貴的學習資源 :
免費贈送 :《尼恩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。
技術自由的實現路徑:
實現你的 架構自由:
《阿里二面:千萬級、億級數據,如何性能優化? 教科書級 答案來了》
《峯值21WQps、億級DAU,小遊戲《羊了個羊》是怎麼架構的?》
… 更多架構文章,正在添加中
實現你的 響應式 自由:
這是老版本 《Flux、Mono、Reactor 實戰(史上最全)》
實現你的 spring cloud 自由:
《Spring cloud Alibaba 學習聖經》 PDF
《分庫分表 Sharding-JDBC 底層原理、核心實戰(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之間混亂關係(史上最全)》
實現你的 linux 自由:
實現你的 網絡 自由:
《網絡三張表:ARP表, MAC表, 路由表,實現你的網絡自由!!》
實現你的 分佈式鎖 自由:
實現你的 王者組件 自由:
《隊列之王: Disruptor 原理、架構、源碼 一文穿透》
《緩存之王:Caffeine 源碼、架構、原理(史上最全,10W字 超級長文)》
《Java Agent 探針、字節碼增強 ByteBuddy(史上最全)》