本文主要介紹泛型誕生的前世今生,特性,以及著名PECS原則的由來。
在日常開發(fā)中,必不可少的會使用到泛型,這個過程中經(jīng)常會出現(xiàn)類似“為什么這樣會編譯報錯?”,“為什么這個列表無法添加元素?”的問題,也會出現(xiàn)感嘆Java的泛型限制太多了很難用的情況。
為了更好的使用泛型,就需要更深地了解它,因此本文主要介紹泛型誕生的前世今生,特性,以及著名PECS原則的由來。
泛型的誕生
背景
在沒有泛型之前,必須使用Object編寫適用于多種類型的代碼,想想就令人頭疼,并且非常的不安全。同時由于數(shù)組的存在,設(shè)計者為了讓其可以比較通用的進(jìn)行處理,也讓數(shù)組允許協(xié)變,這又為程序添加了一些天然的不安全因素。為了解決這些情況,Java的設(shè)計者終于在Java5中引入泛型,然而,正是因為引入泛型的時機(jī)較晚,為了兼容先前的代碼,設(shè)計者也不得不做出一些限制,來讓使用者(也就是我們)以難受換來一些安全。
優(yōu)點
簡單來說,泛型的引入有以下好處:
程序更加易讀
安全性有所保證
以ArrayList舉例,在增加泛型類之前,其通用性是用繼承來實現(xiàn)的,ArrayList類只維護(hù)一個Object引用的數(shù)組,當(dāng)我們使用這個工具類時,想要獲取指定類型的對象必須經(jīng)過強(qiáng)轉(zhuǎn):
import java.util.ArrayList; import java.util.Date; public class Main { public static void main(String[] args) { ArrayList list = new ArrayList(); //強(qiáng)制類型轉(zhuǎn)換 String res = (String) list.get(0); //十分不安全的行為 list.add(new Date()); } }
這種寫法在編譯類型時不會報錯,但一旦使用get獲取結(jié)果并試圖將Date轉(zhuǎn)換為其他類型時,很有可能出現(xiàn)類型轉(zhuǎn)換異常,為了解決這種情況,類型參數(shù)應(yīng)用而生。
類型參數(shù)
類型參數(shù)(Type parameter)使得ArrayList以及其他可能用到的集合類能夠方便的指示虛擬機(jī)其包含元素的類型:?
import java.util.ArrayList; public class Main { public static void main(String[] args) { ArrayListobjects = new ArrayList<>(); objects.add("Hello"); } }
這使得代碼具有更好的可讀性,并且在調(diào)用get()的時候,無需進(jìn)行強(qiáng)轉(zhuǎn),最重要的是,編譯器終于可以檢查一個插入操作是否符合要求,運行時可能出現(xiàn)的各種類型轉(zhuǎn)換錯誤得以在編譯階段就被阻止。
import java.util.ArrayList; import java.util.Date; public class Main { public static void main(String[] args) { ArrayListobjects = new ArrayList<>(); //we can do it like that objects.add("Hello"); //wrong example objects.add(new Date()); } }
基本用法
一般來說,使用泛型工具類很容易,但是自己編寫會相對困難很多,設(shè)計者必須考慮的相當(dāng)周全才能使自己的泛型類庫比較完善。
泛型類
泛型類是有一個或者多個類型變量的類,泛型類中的屬性可以全都不是泛型,不過一般不會這樣做,畢竟類型變量在整個類上定義就是用于指定方法的返回類型以及字段的類型,定義代碼如下:?
public class Animal{ private String name; private T mouth; public T getMouth(){ return mouth; } }
泛型類可以有多個類型變量:
public class Animal{ private String name; private T mouth; private U eyes; public T getMouth(){ return mouth; } }
泛型方法
泛型方法可以在普通類中定義,也可以在泛型類中定義,例如:
public class Animal{ private T value; public static T get(T... a){ return a[a.length-1]; } public T getFirst(){ return value; } }
類型擦除
虛擬機(jī)沒有泛型類型對象,也就是說,所有對象在虛擬機(jī)中都屬于普通類,這意味著在程序編譯并運行后我們的類型變量會被擦除(erased)并替換為限定類型,擦掉類型參數(shù)后的類型就叫做原始類型(raw type),正是因為有類型參數(shù),所以下面的比較結(jié)果會為true:
?
這里的替換規(guī)則我個人理解為:“替換最近上界”,也就是無限定符修飾,則為頂級父類Object,如果有,則會替換為其指定的類型。最直觀的示例如下,這就是類型擦除的體現(xiàn):
?
??
前面說過,泛型是在1.5才提出的,因此類型擦除的目的就是為了保證已有的代碼和類文件依然合法,也就是向低版本兼容。這樣做會帶來幾個問題:
1.類型參數(shù)不支持基本類型,只支持引用類型,這是因為泛型會被擦除為具體類型,而Object不能存儲基本類型的值。
運行時你只能對原始類型進(jìn)行類型檢測:
?
2.不能實例化類型參數(shù)
不能實例化泛型數(shù)組,因為類型擦除會將數(shù)組變?yōu)镺bject數(shù)組,如果允許實例化,極易造成類型轉(zhuǎn)換異常。
強(qiáng)制轉(zhuǎn)換
在編寫泛型方法調(diào)用時,如果擦出了返回類型,編譯器會插入強(qiáng)制類型轉(zhuǎn)換。例如下面的代碼:
public class Main { public static void main(String[] args) { Animalpair = new Animal<>(); Integer first = pair.getFirst(); } }
getFirst擦除類型后的返回類型是Object,編譯器自動插入轉(zhuǎn)換到Integer的強(qiáng)制類型轉(zhuǎn)換,也就是說,編譯器把這個方法調(diào)用轉(zhuǎn)換為兩條虛擬機(jī)指令:
對原始方法的調(diào)用。
將返回的Object類型強(qiáng)制轉(zhuǎn)換為Integer類型。
方法橋接
子類重寫父類方法時,必須和父類保持相同的方法名稱,參數(shù)列表和返回類型。那么問題來了,如果按照之前的思路來講,當(dāng)泛型父類或接口的類型參數(shù)被擦除了,那么子類豈不是不構(gòu)成重寫條件?(參數(shù)類型很可能變化):
擦除前:
?
擦除后:
?
為了解決這個事情,Java引入了橋接方法,為每個繼承/實現(xiàn)泛型類/接口的子類服務(wù),以此保持多態(tài)性,字節(jié)碼如下:
?
(圖片來源:RudeCrab)
其實現(xiàn)原理,就是重寫擦除后的父類方法,并在其內(nèi)部委托了原始的子類方法,巧妙繞過了擦除帶來的影響。不僅如此,就算不是泛型類,當(dāng)子類方法重寫父類方法的返回類型是父類返回類型的子類時,編譯器也會生成橋接方法來滿足重寫的規(guī)則。
總結(jié)
Java核心技術(shù)中總結(jié)的非常到位:
虛擬機(jī)中沒有泛型,只有普通的類和方法。
所有的類型參數(shù)都會替換為他們的限定類型。
會合成橋接方法來保持多態(tài)。
為保持類型安全性,必要時會插入強(qiáng)制類型轉(zhuǎn)換。
變型(Variant)與數(shù)組
變型是類型系統(tǒng)中很重要的概念,主要有三個規(guī)則協(xié)變,逆變,和不變:
?
這三個類型可以解釋為:假設(shè)有一個類型構(gòu)造器f,它可以將已知類型轉(zhuǎn)換為另一種類型,那么,有Animal父類和Dog子類。
則f(Dog)是f(Animal)的子類,稱為協(xié)變;
則f(Dog)是f(Animal)的父類,成為逆變;
則f(Dog)和f(Animal)沒有任何關(guān)系;
而這個f(),可以是泛型,可以是數(shù)組,也可以是方法。
知道了以上概念,我們需要直接指出,泛型默認(rèn)是不支持協(xié)變的,原因很簡單,類型安全:如果允許協(xié)變,可能會造成類型轉(zhuǎn)換異常。而數(shù)組支持協(xié)變,正如文章開頭所說,就是設(shè)計者希望可以對數(shù)組進(jìn)行比較通用的處理,防止方法為每一種類型編寫重復(fù)邏輯,這樣做也確實導(dǎo)致為數(shù)組賦值元素時可能會拋出運行時異常ArrayStoreException,這是一個很危險的坑。Effective Java中直接指出允許數(shù)組協(xié)變是Java的缺陷,我想這也是要多用列表而不用數(shù)組的原因之一。
泛型協(xié)變—PECS原則
為了讓泛型也支持多態(tài),讓其支持協(xié)變是很必要的,最常用的場景:我們想讓一個方法接受一個集合,并做統(tǒng)一的邏輯處理,如果泛型不支持協(xié)變,這種很基本的需求都會成為奢望。
上界
讓泛型支持協(xié)變很簡單,只需要使用? extends的組合即可實現(xiàn),?稱為通配符,這種組合方式聲明了類型的上界,標(biāo)識泛型可接受的類型只能是指定類型或是其子類。在這里,ElectricVehicle和Diesel均是繼承自Car。
?
為了杜絕可協(xié)變后出現(xiàn)類似于數(shù)組一樣的安全隱患,泛型設(shè)計采用了“一刀切”的方式,即:只要聲明了上界,除了null之外,一律不準(zhǔn)傳入給泛型。說白了,就是只讀不寫,這樣當(dāng)然可以保證安全性。
?
到這里可以順便說一下集合的設(shè)計,可以注意到集合中只有add方法是泛型參數(shù),而其余方法并不是,為何要這樣設(shè)計,為何不把其余方法的參數(shù)類型也改為E?其原因就是在于,如果將contains和remove改為E,那么聲明上界之后,調(diào)用這兩個方法會引發(fā)編譯錯誤,然而這兩個方法均為類型安全方法,自然不可聲明為E,add作為很明顯的寫方法,自然也需要用E作為參數(shù)類型,到這里,不得不感嘆類庫設(shè)計者的想法獨到。
?
下界
對應(yīng)協(xié)變的上界,自然有逆變的下界,很自然的,我們使用? super的組合來聲明一個泛型的下界,來表示可以接收本類型或者其父類型。
?
而且相對應(yīng)的,正是由于最多只能接收父類型泛型,所以不會有類型轉(zhuǎn)換失敗的風(fēng)險,因此逆變可以添加元素,不過添加的元素類型只能是指定類型和其子類,切記不要把添加元素和接收泛型類參數(shù)給弄混了。
有利有弊,雖然逆變沒有了協(xié)變只讀不寫的限制,但是讀取元素時將不能確定具體的類型,只能用Object來接收:
?
PECS
正如上面對上下界的描述,我們已經(jīng)明白了大致的應(yīng)用場景,當(dāng)我們需要只讀不寫時,就用協(xié)變,只寫不讀,就用逆變。又想讀又想寫,我們應(yīng)該指明準(zhǔn)確的泛型類型。
注明的PECS原則就總結(jié)了這一點,PECS(Prodcuer extends Consumer super),也就是說,作為元素的生產(chǎn)者Prodcuer,要用協(xié)變,支持元素的讀取,而作為消費者Consumer,要支持逆變,支持元素的寫入。
?
Collections的copy方法就非常好的印證了這一點:
-
JAVA
+關(guān)注
關(guān)注
19文章
2966瀏覽量
104700 -
程序
+關(guān)注
關(guān)注
117文章
3785瀏覽量
81000 -
specs
+關(guān)注
關(guān)注
0文章
5瀏覽量
1583
原文標(biāo)題:深入了解Java泛型——從前世今生到PECS原則
文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論