說來慚愧,雖然平時(shí)經(jīng)常會(huì)使用到一些泛型類,但是卻一直沒有深入地去了解過泛型機(jī)制。今天開始學(xué)習(xí)記錄泛型機(jī)制相關(guān)的知識(shí)點(diǎn)。
由于本人水平有限,如果有理解錯(cuò)誤的地方,歡迎指出。
參考書籍《Java核心技術(shù)卷I 基礎(chǔ)知識(shí)》
在開始學(xué)習(xí)泛型程序設(shè)計(jì)之前,我們首先需要弄清楚一個(gè)最基本的問題:Java語言設(shè)計(jì)者為什么要在Java中引入泛型機(jī)制?加入泛型機(jī)制解決了什么問題?
帶著這兩個(gè)疑問,開始泛型機(jī)制的學(xué)習(xí)之路。
一、為什么要使用泛型程序設(shè)計(jì)
泛型程序設(shè)計(jì)引入的初衷是為了使開發(fā)者編寫的代碼被不同對(duì)象的類型重用。大家可能會(huì)覺得,這種描述也太抽象了吧!沒關(guān)系,接下來咱們分析一個(gè)簡單例子。
ArrayList這個(gè)容器類對(duì)Java開發(fā)者來說都太熟悉了,我們不僅可以用它來存儲(chǔ)String、Integer這些系統(tǒng)定義好的類型的對(duì)象,也可以存儲(chǔ)自定義的類型的對(duì)象,那么ArrayList是如何做到的呢?
在泛型機(jī)制出現(xiàn)以前,泛型程序設(shè)計(jì)是利用繼承來實(shí)現(xiàn)的。Object類是所有類的基類,ArrayList通過維護(hù)Object引用的數(shù)組來達(dá)到存儲(chǔ)不同類型對(duì)象的目的,實(shí)際上就是利用對(duì)象的向上轉(zhuǎn)型。看一個(gè)簡單的例子:
/**
* 偽代碼
*/
public class MyList {
private Object[] elements;
public Object get(int i) {
return elements[i];
}
public void add(Object o) {
elements[elements.length] = o;
}
}
這樣做確實(shí)可以實(shí)現(xiàn)存儲(chǔ)多種類型的對(duì)象,但是可能會(huì)有兩個(gè)問題:
- 當(dāng)你從MyList 中獲取一個(gè)值,必須要強(qiáng)制轉(zhuǎn)換成我們需要的類型對(duì)象
MyList list = new MyList();
list.add("ABCD");
String s = (String) list.get(0); //必須強(qiáng)制轉(zhuǎn)換為String類型
- 我們可以向MyList 添加任何類型的對(duì)象,這樣可能會(huì)導(dǎo)致獲取某個(gè)值時(shí)強(qiáng)制轉(zhuǎn)換發(fā)生錯(cuò)誤
MyList list = new MyList();
list.add(123);
String s = (String) list.get(0); //類型轉(zhuǎn)換失敗
對(duì)于上面這些問題,泛型為我們提供了一個(gè)很好的解決方案:類型參數(shù)。ArrayList會(huì)使用一個(gè)類型參數(shù)來指定它存儲(chǔ)的對(duì)象類型。
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("ABC");
// arrayList.add(123); //無法通過編譯
String s = arrayList.get(0);
使用了類型參數(shù)之后,當(dāng)調(diào)用get方法的時(shí)候,不需要進(jìn)行強(qiáng)制轉(zhuǎn)換,編譯器就知道你要返回的是什么類型的對(duì)象。當(dāng)你向其中插入錯(cuò)誤的參數(shù)類型的時(shí)候,是無法通過編譯的。很顯然,出現(xiàn)編譯錯(cuò)誤比在運(yùn)行的時(shí)候出現(xiàn)類型轉(zhuǎn)換異常代價(jià)要小得多。
類型參數(shù)的魅力在于:使程序具有更好的可讀性和安全性。
到這里為止,基本上已經(jīng)明白了為什么要使用泛型程序設(shè)計(jì)。實(shí)際上,想要設(shè)計(jì)一個(gè)完美的泛型類并不容易,因?yàn)榫帉懸粋€(gè)泛型類通常需要盡可能地預(yù)測出泛型類未來可能出現(xiàn)的應(yīng)用場景,這也是泛型技術(shù)的一個(gè)難點(diǎn),此處暫時(shí)不做深入討論,咱還是老老實(shí)實(shí)由淺入深地學(xué)習(xí)吧。
二、泛型類
Java核心技術(shù)對(duì)泛型類的定義:一個(gè)泛型類就是具有一個(gè)或多個(gè)類型變量的類。
這樣描述可能有點(diǎn)抽象,還是先看一個(gè)具體的例子吧!依然還是以ArrayList為例,首先來看看它是如何定義的。這里只是為了說明如何定義一個(gè)泛型類,所以只摘取ArrayList的部分代碼。
public class ArrayList<E> {
public boolean add(E e) {
……
}
public E get(int index) {
……
}
public E remove(int index) {
……
}
}
定義泛型類首先需要引入類型變量,并且用<>括起來直接放在類名后面ArrayList<E>。類型變量通常使用比較短的大寫字母表示,例如K和V,可以用來代表key和value。
泛型類也可以有多個(gè)類型變量,多個(gè)變量之間用","隔開,例如public class ClassName<T, K>。類型變量通常用于指定方法的返回類型或者變量的類型,例如上面代碼中get()方法返回值以及add()方法參數(shù)變量類型都指定是E。
定義一個(gè)泛型類當(dāng)然是供開發(fā)者使用的啦!使用方法也很簡單,只需要用具體的類型替換類型變量即可。例如用String替換類型變量E
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("XXX");
String s = arrayList.get(0);
arrayList.remove("XXX");
從表面上看,Java的泛型類有點(diǎn)類似于C++的模板類,但是這兩種機(jī)制是有著本質(zhì)區(qū)別的。
三、泛型方法
現(xiàn)在,我們已經(jīng)學(xué)會(huì)了如何定義一個(gè)泛型類,接下來繼續(xù)學(xué)習(xí)泛型方法的定義和使用。在開始學(xué)習(xí)之前,我們需要明確的一點(diǎn)是:泛型方法不僅可以定義在泛型類中,在普通類中也是可以定義泛型方法的。
/**
* 泛型方法
* @param t
* @param <T>
* @return
*/
private static <T> T getT(T t) {
return t;
}
上面是一個(gè)簡單的泛型方法定義示例。定義一個(gè)泛型方法很簡單,我們只需要把類型變量放在修飾符后面,返回類型的前面即可。調(diào)用泛型方法也很簡單,我們只需要提前指定泛型變量的類型,除此之外與普通方法的調(diào)用并不差別,標(biāo)準(zhǔn)的調(diào)用方法如下:
Test.<String>getT("aaaa");
通常情況下,我們不需要提前指定泛型變量的類型,編譯器擁有足夠的信息自動(dòng)判斷出泛型變量的具體類型。也就是說編譯器會(huì)通過參數(shù)類型為String來判斷出T一定是String類型,因此我們可以直接這樣使用方法:
Test.getT("aaaa");
但是,如果我們無法為編譯器提供足夠的信息判斷出泛型變量的類型,還是需要主動(dòng)指定泛型變量的具體類型。
四、泛型接口
定義一個(gè)泛型接口非常簡單:
public interface Person<T> {
T name();
}
然后在實(shí)現(xiàn)這個(gè)泛型接口的時(shí)候指定泛型T的具體類型即可。
public class Student implements Person<String> {
@Override
public String name() {
return "I'm Jack!";
}
}
五、類型變量的限定
有時(shí)候,類或方法需要對(duì)類型變量加以約束。舉個(gè)例子,假如某家公司需要招聘一名Android開發(fā)工程師,那么招聘的流程可能是像下面這樣的:
/**
* @param android Android開發(fā)人員
* @param <ANDROID> 從事Android開發(fā)的人群
*/
private static <ANDROID> ANDROID getAndroidDeveloper(ANDROID android) {
return android;
}
上面這段代碼意味著只要你能夠完成Android開發(fā)工作,你都符合這家公司的招聘條件。幾天之后,技術(shù)主管告訴HR,公司需要的是能夠熟練使用Kotlin語言進(jìn)行開發(fā)的技術(shù)人員,因此HR需要在招聘信息上對(duì)職位增加一條約束信息,也就是對(duì)類型變量ANDROID設(shè)置一個(gè)限定。限定格式:<ANDROID extends UseKotlin>
/**
* @param android Android開發(fā)人員
* @param <ANDROID> 從事Android開發(fā)的人群
*/
private static <ANDROID extends UseKotlin> ANDROID getAndroidDeveloper(ANDROID android) {
return android;
}
/**
* 使用Kotlin
*/
public interface UseKotlin { }
現(xiàn)在,將ANDROID限制為必須是實(shí)現(xiàn)了UseKotlin接口的類型,這也就意味著應(yīng)聘者必須是能夠使用Kotlin進(jìn)行Android開發(fā)的技術(shù)人員。當(dāng)然,也可以給變量類型設(shè)置多個(gè)限定,多個(gè)限定類型之間用"&"分隔,例如公司還要求應(yīng)聘者具有三年工作經(jīng)驗(yàn)。多限定格式:<ANDROID extends UseKotlin & ThreeYearExperience>
/**
* @param android Android開發(fā)人員
* @param <ANDROID> 從事Android開發(fā)的人群
*/
private static <ANDROID extends UseKotlin & ThreeYearExperience> ANDROID getAndroidDeveloper(ANDROID android) {
return android;
}
/**
* 使用Kotlin
*/
public interface UseKotlin {
}
/**
* 三年開發(fā)經(jīng)驗(yàn)
*/
public interface ThreeYearExperience {
}
需要注意的是,如果類型變量擁有多個(gè)限定,那么限定中最多只能有一個(gè)類,并且這個(gè)類必須是限定列表中的第一個(gè)。例如現(xiàn)在要求應(yīng)聘者擁有本科學(xué)歷,本科學(xué)歷是一個(gè)類。
/**
* 本科學(xué)歷
*/
public class Benke {
}
那么限定格式必須是<ANDROID extends Benke & UseKotlin & ThreeYearExperience>,Benke這個(gè)限定必須放在最前面,否則將無法通過編譯。
六、泛型擦除
在Java虛擬機(jī)中,是沒有泛型類型對(duì)象的,所有對(duì)象都屬于普通類。
在C++模板中,編譯器使用提供的類型參數(shù)來擴(kuò)充模板,因此List<A>和 List<B>實(shí)際上會(huì)生成兩套不同的代碼。而 Java 中的泛型以不同的方式實(shí)現(xiàn),編譯器會(huì)對(duì)這些類型參數(shù)進(jìn)行擦除和替換,因此類型 ArrayList<Integer> 和 ArrayList<String> 的對(duì)象共享相同的類ArrayList。
在C++中每個(gè)模板的實(shí)例化都會(huì)產(chǎn)生不同的類型,這一現(xiàn)象被稱為“模板代碼膨脹”,而java則不存在這個(gè)問題的困擾。Java虛擬機(jī)中沒有泛型,只有基本類型和類類型,泛型會(huì)被擦除,一般會(huì)被修改為Object,如果有限制,例如<T extends Comparable> 會(huì)被修改為Comparable。
在C++中不能對(duì)模板參數(shù)的類型加以限制,如果程序員用一個(gè)不適當(dāng)?shù)念愋蛯?shí)例化一個(gè)模板,將會(huì)在模板代碼中報(bào)告一個(gè)錯(cuò)誤信息。
這樣描述可能還是有點(diǎn)抽象,還是舉個(gè)簡單的例子吧!
public class ArrayList<E> {
public boolean add(E e) {
……
}
public E get(int index) {
……
}
public E remove(int index) {
……
}
}
對(duì)于ArrayList類而言,它在虛擬機(jī)中以原始類型的形式存在。由于E是一個(gè)無限定的類型變量,所以在原始類型中直接用Object替換E。因此ArrayList<E> 的原始類型如下:
public class ArrayList {
public boolean add(Object e) {
……
}
public Object get(int index) {
……
}
public Object remove(int index) {
……
}
}
翻譯泛型表達(dá)式
當(dāng)程序調(diào)用泛型方法時(shí),如果擦除返回類型,那么編譯器會(huì)插入強(qiáng)制類型轉(zhuǎn)換
繼續(xù)分析下面這個(gè)例子:
ArrayList<String> arrayList = new ArrayList<>();
……
String s = arrayList.get(0);
我們現(xiàn)在已經(jīng)知道,編譯器在擦除get()方法后返回的是Object類型對(duì)象,然后自動(dòng)插入String 的強(qiáng)制類型轉(zhuǎn)換,也就是把這個(gè)方法調(diào)用分成兩個(gè)步驟執(zhí)行:
- 擦除方法返回類型并返回Object類型對(duì)象
- 將返回的Object對(duì)象強(qiáng)制轉(zhuǎn)換成String類型
翻譯泛型方法
與泛型表達(dá)式類似,類型擦除也會(huì)發(fā)生在泛型方法中。
我們來看一個(gè)簡單的泛型方法:
/**
* 泛型方法
*/
private static <T> T getT(T t) {
return t;
}
擦除類型之后:
/**
* 擦除類型后的泛型方法
*/
private static Object getT(Object t) {
return t;
}
根據(jù)擦除規(guī)則,由于類型變量T無任何限定類型,因此直接被Object替換。
方法的類型擦除會(huì)帶來兩個(gè)復(fù)雜的問題。
1. 類型擦除與多態(tài)發(fā)生沖突
public static class Person<T> {
private T name;
public void setName(T name) {
this.name = name;
}
public T getName() {
return name;
}
}
public class Student extends Person<String> {
@Override
public void setName(String name) {
super.setName(name);
}
}
對(duì)于上面這段代碼,當(dāng)Person類被擦除后,會(huì)存在另一個(gè)從Person類繼承的setName方法,即
public void setName(Object name)
這個(gè)時(shí)候Student類相當(dāng)于擁有兩個(gè)不同的setName方法,因?yàn)樗鼈冇胁煌愋偷膮?shù),Object和String??紤]下面這段代碼:
Student student = new Student();
Person<String> person = student;
person.setName("Bob");
我們希望對(duì)setName的調(diào)用具有多態(tài)性,并調(diào)用最合適的那個(gè)方法。由于person引用Student對(duì)象,所以應(yīng)該調(diào)用Student.setName,問題在于類型擦除與多態(tài)就發(fā)生了沖突。為了解決這個(gè)問題,需要編譯器在Student 類中生成一個(gè)橋方法:
public void setName(Object name) {
setName((String)name);
}
要想了解它的工作過程,還得從person.setName("Bob")語句的執(zhí)行說起:
變量person已經(jīng)聲明為類型Person<String>,這個(gè)類型只有一個(gè)簡單的方法setName(Object name),虛擬機(jī)用person引用的對(duì)象調(diào)用setName方法,也就是調(diào)用Student.setName(Object name),這就是我們上面所說的橋方法,橋方法內(nèi)部又會(huì)調(diào)用Student.setName(String name)方法。
這樣person最終調(diào)用的就是Student.setName(String name)方法,這正是我們所希望的結(jié)果。
2. 可能會(huì)產(chǎn)生參數(shù)類型相同、返回類型不同的方法
還是以上面的代碼為例,如果Student類中也覆蓋了getName方法,那么擦除類型中會(huì)存在兩個(gè)getName方法:
public String getName() { }
public Object getName() { }
在Java代碼中,具有相同參數(shù)類型的兩個(gè)方法是不合法的,但是在虛擬機(jī)中,用參數(shù)類型和返回類型確定一個(gè)方法,因此虛擬機(jī)可以正確地處理上面這種情況。
總之,我們需要記住有關(guān)Java泛型轉(zhuǎn)換的幾個(gè)結(jié)論:
- 虛擬機(jī)中沒有泛型,只有普通的類和方法
- 在擦除的類型中,所有的類型參數(shù)都用它們的限定類型替換
- 使用橋方法來保持多態(tài)的正確性
- 為保證類型安全,必要時(shí)插入強(qiáng)制類型轉(zhuǎn)換
七、通配符類型
在實(shí)例化對(duì)象的時(shí)候,不確定泛型參數(shù)的具體類型時(shí),可以使用通配符 ? 進(jìn)行對(duì)象定義
- 上邊界限定通配符<? extends Object>
- 下邊界限定通配符<? super Object>
接下來通過幾個(gè)簡單的例子分析通配符具體的使用場景。
1、上邊界限定通配符<? extends Object>
假設(shè)現(xiàn)在有兩個(gè)類,Employee和Manager extends Employee,他們具有繼承關(guān)系:
public class Employee {
public String jobName() {
return "Employee";
}
}
public class Manager extends Employee{
@Override
public String jobName() {
return "Manager";
}
}
現(xiàn)在需要編寫一個(gè)打印雇員信息的方法:
public static void printJob(List<Employee> employeeList){
for (int i = 0; i < employeeList.size(); i++) {
Employee employee = employeeList.get(i);
System.out.println("employee name = "+employee.jobName());
}
}
這個(gè)方法可以成功打印出雇員信息,我們顯然不能把List<Manager>參數(shù)傳遞給這個(gè)方法,這也就意味著如果我們想打印Manager相關(guān)的信息,需要再編寫一個(gè)方法printJob(List<Manager> managerList)。通配符類型很好地解決了這個(gè)問題:
public static void printJob(List<? extends Employee> employeeList) {
……
}
這樣printJob()方法不僅能夠接收 List<Employee> 類型的參數(shù),也能夠接收 List<Manager> 類型的參數(shù),因?yàn)?code>Manager extends Employee。這就是上邊界限定通配符的作用。
上邊界限定通配符不允許對(duì)
List<? extends Employee> employeeList進(jìn)行任何更改操作,這是因?yàn)榫幾g器只知道需要某個(gè)Employee的子類型,但不知道具體是什么類型。這也是引入有限定的通配符的關(guān)鍵之處,現(xiàn)在已經(jīng)有辦法區(qū)分安全的訪問器方法和不安全的更改器方法。
2、下邊界限定通配符<? super Employee>
下邊界限定通配符與上邊界限定通配符原理類似,只不過它表達(dá)的是相反的概念。
public class Person {
public String jobName() {
return "Person";
}
}
public class Employee extends Person {
public String jobName() {
return "Employee";
}
}
public static void printJob(List<? super Employee> employeeList) {
……
}
下邊界限定通配符允許對(duì)
List<? super Employee> employeeList進(jìn)行更改操作,只要我們操作的是Employee的基類對(duì)象。但往外取元素只能使用Object對(duì)象接收,因?yàn)榫幾g器只知道需要Employee的父類型,但不知道具體是什么類型,這樣元素的類型信息就全部丟失。
3、限定通配符總結(jié)
帶有超類型限定的通配符可以向泛型對(duì)象寫入,帶有子類型限定的通配符可以從泛型對(duì)象讀取。
Joshua Bloch 稱那些你只能從中讀取的對(duì)象為生產(chǎn)者,并稱那些你只能寫入的對(duì)象為消費(fèi)者。他建議:“為了靈活性最大化,在表示生產(chǎn)者或消費(fèi)者的輸入?yún)?shù)上使用通配符類型”,并提出了以下助記符:
PECS 代表生產(chǎn)者-Extends,消費(fèi)者-Super(Producer-Extends, Consumer-Super)。
注意:如果你使用一個(gè)生產(chǎn)者對(duì)象,如 List<? extends Foo>,在該對(duì)象上不允許調(diào)用 add() 或 set()。但這并不意味著該對(duì)象是不可變的:例如,沒有什么阻止你調(diào)用 clear()從列表中刪除所有項(xiàng)目,因?yàn)?clear() 根本無需任何參數(shù)。通配符(或其他類型的型變)保證的唯一的事情是類型安全。不可變性完全是另一回事。
- 如果你想從列表中讀取T類型的元素,你需要把這個(gè)列表聲明成
<? extends T>,例如List<? extends Employee> employeeList。但是不能往該列表中添加任何元素,因?yàn)榫幾g器只知道需要某個(gè)Employee的子類型,但不知道具體是什么類型。 - 如果你想把T類型的元素加入到列表中,你需要把這個(gè)列表聲明成
<? super T>,比如List<? super Employee>。由于編譯器無法保證從中讀取到的元素的具體類型,所以只能用Object對(duì)象接收讀取到的元素,這樣會(huì)導(dǎo)致一定程度上的信息丟失。