為什么我們需要泛型坐求?
通過(guò)兩段代碼我們就可以知道為何我們需要泛型
實(shí)際開(kāi)發(fā)中,經(jīng)常有數(shù)值類型求和的需求晌梨,例如實(shí)現(xiàn)int類型的加法, 有時(shí)候還需要實(shí)現(xiàn)long類型的求和, 如果還需要double類型的求和桥嗤,需要重新在重載一個(gè)輸入是double類型的add方法。這樣方法就顯得多余和重復(fù)了仔蝌。
- 定義一個(gè)List類型的集合泛领,先向其中加入了兩個(gè)字符串類型的值,隨后加入一個(gè)Integer類型的值敛惊。這是完全允許的渊鞋,因?yàn)榇藭r(shí)list默認(rèn)的類型為Object類型。在之后的循環(huán)中豆混,由于忘記了之前在list中也加入了Integer類型的值或其他編碼原因篓像,很容易出現(xiàn)類似于//上圖中的錯(cuò)誤。因?yàn)榫幾g階段正常皿伺,而運(yùn)行時(shí)會(huì)出現(xiàn)“java.lang.ClassCastException”異常员辩。因?yàn)閷?dǎo)致此類錯(cuò)誤編碼過(guò)程中不易發(fā)現(xiàn)。
在如上的編碼過(guò)程中鸵鸥,我們發(fā)現(xiàn)主要存在兩個(gè)問(wèn)題:
- 當(dāng)我們將一個(gè)對(duì)象放入集合中奠滑,集合不會(huì)記住此對(duì)象的類型丹皱,當(dāng)再次從集合中取出此對(duì)象時(shí),該對(duì)象的編譯類型變成了Object類型宋税,但其運(yùn)行時(shí)類型仍然為其本身類型摊崭。
- 因此,//上圖 取出集合元素時(shí)需要人為的強(qiáng)制類型轉(zhuǎn)化到具體的目標(biāo)類型杰赛,且很容易出現(xiàn)“java.lang.ClassCastException”異常呢簸。所以泛型的好處就是:
1)適用于多種數(shù)據(jù)類型執(zhí)行相同的代碼
2)泛型中的類型在使用時(shí)指定,不需要強(qiáng)制類型轉(zhuǎn)換
泛型類和泛型接口
泛型乏屯,即“參數(shù)化類型”根时。一提到參數(shù)拯啦,最熟悉的就是定義方法時(shí)有形參循头,然后調(diào)用此方法時(shí)傳遞實(shí)參憎瘸。那么參數(shù)化類型怎么理解呢肺稀?
顧名思義,就是將類型由原來(lái)的具體的類型參數(shù)化改执,類似于方法中的變量參數(shù)体谒,此時(shí)類型也定義成參數(shù)形式(可以稱之為類型形參)填抬,然后在使用/調(diào)用時(shí)傳入具體的類型(類型實(shí)參)窘问。
泛型的本質(zhì)是為了參數(shù)化類型(在不創(chuàng)建新的類型的情況下辆童,通過(guò)泛型指定的不同類型來(lái)控制形參具體限制的類型)。也就是說(shuō)在泛型使用過(guò)程中南缓,操作的數(shù)據(jù)類型被指定為一個(gè)參數(shù)胸遇,這種參數(shù)類型可以用在類、接口和方法中汉形,分別被稱為泛型類、泛型接口倍阐、泛型方法概疆。
引入一個(gè)類型變量T(其他大寫字母都可以,不過(guò)常用的就是T峰搪,E岔冀,K,V等等)概耻,并且用<>括起來(lái)使套,并放在類名的后面。泛型類是允許有多個(gè)類型變量的鞠柄。
public class GenericClass<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
public class GenericClass2<T,K> {
private T data;
private K key;
public GenericClass(T data, K key) {
this.data = data;
this.key = key;
}
}
泛型接口與泛型類的定義基本相同侦高。
public interface GenericInterface<T> {
T onNext();
}
而實(shí)現(xiàn)泛型接口的類,有兩種實(shí)現(xiàn)方法:
1厌杜、未傳入泛型實(shí)參時(shí):
public class ImplGenericInterface<T> implements GenericInterface<T> {
@Override
public T onNext() {
return null;
}
}
在new出類的實(shí)例時(shí)奉呛,需要指定具體類型:
ImplGenericInterface<String> implGenericInterface = new ImplGenericInterface<>();
2计螺、傳入泛型實(shí)參
public class ImplGenericInterface2 implements GenericInterface<String> {
@Override
public String onNext() {
return null;
}
}
在new出類的實(shí)例時(shí),和普通的類沒(méi)區(qū)別瞧壮。
泛型方法
/**
* @param <T> 泛型方法的特定寫法
* @return 返回參數(shù)類型是傳入的參數(shù)類型
*/
private <T> T getMiddleValue(T... a){
return a[a.length/2];
}
泛型方法登馒,是在調(diào)用方法的時(shí)候指明泛型的具體類型 ,泛型方法可以在任何地方和任何場(chǎng)景中使用咆槽,包括普通類和泛型類陈轿。注意泛型類中定義的普通方法和泛型方法的區(qū)別
普通方法:
public class GenericClass<T> {
private T data;
/**
* 這個(gè)方法中雖然使用了泛型作為返回參數(shù)類型
* 但是這個(gè)方法只是 GenericClass 類的普通成員方法
* 在權(quán)限修飾符 和 返回參數(shù)類型之間還有一個(gè) <T> 才是泛型方法的標(biāo)示(T和定義的泛型表達(dá)一致)
*/
public T getData() {
return data;
}
}
泛型方法
public class GenericClass<T> {
private T data;
/**
* 這個(gè)方法中雖然使用了泛型作為返回參數(shù)類型
* 但是這個(gè)方法只是 GenericClass 類的普通成員方法
* 在權(quán)限修飾符 和 返回參數(shù)類型之間還有一個(gè) <T> 才是泛型方法的標(biāo)示(T和定義的泛型表達(dá)一致)
*/
public T getData() {
return data;
}
/**
* 泛型方法 和類中的泛型 無(wú)關(guān)聯(lián)
* 有泛型方法的特定標(biāo)示<E>
*/
public <E> E testGeneric(E e){
//對(duì)E 進(jìn)行相應(yīng)的操作
return e;
}
}
限定類型變量
有時(shí)候,我們需要對(duì)類型變量加以約束秦忿,比如計(jì)算兩個(gè)變量的最小济欢,最大值。
請(qǐng)問(wèn)小渊,如果確保傳入的兩個(gè)變量一定有compareTo方法法褥?那么解決這個(gè)問(wèn)題的方案就是將T限制為實(shí)現(xiàn)了接口Comparable的類
public static <T extends Comparable<T>> T min(@NonNull T a, @NonNull T b){
if(a.compareTo(b) > 0){
return b;
}else {
return a;
}
}
T extends Comparable中
T表示應(yīng)該綁定類型的子類型,Comparable表示綁定類型酬屉,子類型和綁定類型可以是類也可以是接口半等。
如果這個(gè)時(shí)候,我們?cè)噲D傳入一個(gè)沒(méi)有實(shí)現(xiàn)接口Comparable的類的實(shí)例呐萨,將會(huì)發(fā)生編譯錯(cuò)誤杀饵。
同時(shí)extends左右都允許有多個(gè),如 T,V extends Comparable & Serializable
注意限定類型中谬擦,只允許有一個(gè)類切距,而且如果有類,這個(gè)類必須是限定列表的第一個(gè)惨远。
這種類的限定既可以用在泛型方法上也可以用在泛型類上谜悟。
泛型中的約束和局限性
現(xiàn)在我們有泛型類
public class Restrict<T> {
private T data;
public Restrict(T data) {
this.data = data;
}
}
不能用基本類型實(shí)例化類型參數(shù)
運(yùn)行時(shí)類型查詢只適用于原始類型
泛型類的靜態(tài)上下文中類型變量失效
不能在靜態(tài)域或方法中引用類型變量。因?yàn)榉盒褪且趯?duì)象創(chuàng)建的時(shí)候才知道是什么類型的北秽,而對(duì)象創(chuàng)建的代碼執(zhí)行先后順序是static的部分葡幸,然后才是構(gòu)造函數(shù)等等。所以在對(duì)象初始化之前static的部分已經(jīng)執(zhí)行了贺氓,如果你在靜態(tài)部分引用的泛型蔚叨,那么毫無(wú)疑問(wèn)虛擬機(jī)根本不知道是什么東西,因?yàn)檫@個(gè)時(shí)候類還沒(méi)有初始化辙培。
不能創(chuàng)建參數(shù)化類型的數(shù)組
Restrict<Double>[] restrictArray;//可以定義
Restrict<Double>[] restricts = new Restrict<Double>[10];//不能這樣使用
不能實(shí)例化類型變量
不能捕獲泛型類的實(shí)例
但是這樣可以:
泛型類型的繼承規(guī)則
現(xiàn)在我們有一個(gè)類和子類,有一個(gè)泛型類
請(qǐng)問(wèn)Pair<Employee>和Pair<Worker>是繼承關(guān)系嗎蔑水?
答案:不是,他們之間沒(méi)有什么關(guān)系
但是泛型類可以繼承或者擴(kuò)展其他泛型類扬蕊,比如List和ArrayList
Pair<Employee> pair = new ExtentPair<>();
class Pair<T>{
}
class ExtentPair<T> extends Pair<T>{
}
通配符類型
正是因?yàn)榍懊嫠龅牟蟊穑琍air<Employee>和Pair<Worker>沒(méi)有任何關(guān)系,如果我們有一個(gè)泛型類和一個(gè)方法
并有如下使用情況厨相,怎會(huì)產(chǎn)生錯(cuò)誤领曼。
為解決這個(gè)問(wèn)題鸥鹉,于是提出了一個(gè)通配符類型 ?
有兩種使用方式:
? extends X 表示類型的上界庶骄,類型參數(shù)是X的子類
毁渗? super X 表示類型的下界,類型參數(shù)是X的超類
這兩種 方式從名字上來(lái)看单刁,特別是super灸异,很有迷惑性,下面我們來(lái)仔細(xì)辨析這兩種方法羔飞。
肺樟? extends X
表示傳遞給方法的參數(shù),必須是X的子類(包括X本身)
但是對(duì)泛型類Pair來(lái)說(shuō)逻淌,如果其中提供了get和set類型參數(shù)變量的方法的話么伯,set方法是不允許被調(diào)用的,會(huì)出現(xiàn)編譯錯(cuò)誤
get方法則沒(méi)問(wèn)題卡儒,會(huì)返回一個(gè)Employee類型的值田柔。
為什么呢?
道理很簡(jiǎn)單骨望,硬爆? extends X 表示類型的上界,類型參數(shù)是X的子類擎鸠,那么可以肯定的說(shuō)缀磕,get方法返回的一定是個(gè)X(不管是X或者X的子類)編譯器是可以確定知道的。但是set方法只知道傳入的是個(gè)X劣光,至于具體是X的那個(gè)子類袜蚕,不知道。
總結(jié):主要用于安全地訪問(wèn)數(shù)據(jù)赎线,可以訪問(wèn)X及其子類型廷没,并且不能寫入非null的數(shù)據(jù)。
垂寥? super X
表示傳遞給方法的參數(shù),必須是X的超類(包括X本身)
為什么fun1方法不能調(diào)用了另锋,是因?yàn)镋mployee 的超類 Objet 沒(méi)有該方法
改成如下就行了:
private void print(Pair<? super Employee> pair){
Log.e("YLC",pair.data.toString());
}
但是對(duì)泛型類Pair來(lái)說(shuō)滞项,如果其中提供了get和set類型參數(shù)變量的方法的話,set方法可以被調(diào)用的夭坪,且能傳入的參數(shù)只能是X或者X的子類文判,get方法只會(huì)返回一個(gè)Object類型的值。
為何室梅?
戏仓? super X 表示類型的下界疚宇,類型參數(shù)是X的超類(包括X本身),那么可以肯定的說(shuō)赏殃,get方法返回的一定是個(gè)X的超類敷待,那么到底是哪個(gè)超類?不知道仁热,但是可以肯定的說(shuō)榜揖,Object一定是它的超類,所以get方法返回Object抗蠢。編譯器是可以確定知道的举哟。對(duì)于set方法來(lái)說(shuō),編譯器不知道它需要的確切類型迅矛,但是X和X的子類可以安全的轉(zhuǎn)型為X妨猩。
總結(jié):主要用于安全地寫入數(shù)據(jù),可以寫入X及其子類型秽褒。
無(wú)限定的通配符 ?
表示對(duì)類型沒(méi)有什么限制壶硅,可以把?看成所有類型的父類震嫉,如Pair< ?>森瘪;
比如:
ArrayList<T> al=new ArrayList<T>(); 指定集合元素只能是T類型
ArrayList<?> al=new ArrayList<?>();集合元素可以是任意類型,這種沒(méi)有意義票堵,一般是方法中扼睬,只是為了說(shuō)明用法。
在使用上:
悴势? getFirst() : 返回值只能賦給 Object窗宇,;
void setFirst(?) : setFirst 方法不能被調(diào)用特纤, 甚至不能用 Object 調(diào)用军俊;
虛擬機(jī)是如何實(shí)現(xiàn)泛型的?
泛型思想早在C++語(yǔ)言的模板(Template)中就開(kāi)始生根發(fā)芽捧存,在Java語(yǔ)言處于還沒(méi)有出現(xiàn)泛型的版本時(shí)粪躬,只能通過(guò)Object是所有類型的父類和類型強(qiáng)制轉(zhuǎn)換兩個(gè)特點(diǎn)的配合來(lái)實(shí)現(xiàn)類型泛化。昔穴,由于Java語(yǔ)言里面所有的類型都繼承于java.lang.Object镰官,所以O(shè)bject轉(zhuǎn)型成任何對(duì)象都是有可能的。但是也因?yàn)橛袩o(wú)限的可能性吗货,就只有程序員和運(yùn)行期的虛擬機(jī)才知道這個(gè)Object到底是個(gè)什么類型的對(duì)象泳唠。在編譯期間,編譯器無(wú)法檢查這個(gè)Object的強(qiáng)制轉(zhuǎn)型是否成功宙搬,如果僅僅依賴程序員去保障這項(xiàng)操作的正確性笨腥,許多ClassCastException的風(fēng)險(xiǎn)就會(huì)轉(zhuǎn)嫁到程序運(yùn)行期之中拓哺。
泛型技術(shù)在C#和Java之中的使用方式看似相同,但實(shí)現(xiàn)上卻有著根本性的分歧脖母,C#里面泛型無(wú)論在程序源碼中士鸥、編譯后的IL中(Intermediate Language,中間語(yǔ)言镶奉,這時(shí)候泛型是一個(gè)占位符)础淤,或是運(yùn)行期的CLR中,都是切實(shí)存在的哨苛,List<int>與List<String>就是兩個(gè)不同的類型鸽凶,它們?cè)谙到y(tǒng)運(yùn)行期生成,有自己的虛方法表和類型數(shù)據(jù)建峭,這種實(shí)現(xiàn)稱為類型膨脹玻侥,基于這種方法實(shí)現(xiàn)的泛型稱為真實(shí)泛型。
Java語(yǔ)言中的泛型則不一樣亿蒸,它只在程序源碼中存在凑兰,在編譯后的字節(jié)碼文件中,就已經(jīng)替換為原來(lái)的原生類型(Raw Type边锁,也稱為裸類型)了姑食,并且在相應(yīng)的地方插入了強(qiáng)制轉(zhuǎn)型代碼,因此茅坛,對(duì)于運(yùn)行期的Java語(yǔ)言來(lái)說(shuō)音半,ArrayList<int>與ArrayList<String>就是同一個(gè)類,所以泛型技術(shù)實(shí)際上是Java語(yǔ)言的一顆語(yǔ)法糖贡蓖,Java語(yǔ)言中的泛型實(shí)現(xiàn)方法稱為類型擦除曹鸠,基于這種方法實(shí)現(xiàn)的泛型稱為偽泛型。
將一段Java代碼編譯成Class文件斥铺,然后再用字節(jié)碼反編譯工具進(jìn)行反編譯后彻桃,將會(huì)發(fā)現(xiàn)泛型都不見(jiàn)了,程序又變回了Java泛型出現(xiàn)之前的寫法晾蜘,泛型類型都變回了原生類型
上面這段代碼是不能被編譯的邻眷,因?yàn)閰?shù)List<Integer>和List<String>編譯之后都被擦除了,變成了一樣的原生類型List<E>剔交,擦除動(dòng)作導(dǎo)致這兩種方法的特征簽名變得一模一樣耗溜。
由于Java泛型的引入,各種場(chǎng)景(虛擬機(jī)解析省容、反射等)下的方法調(diào)用都可能對(duì)原有的基礎(chǔ)產(chǎn)生影響和新的需求,如在泛型類中如何獲取傳入的參數(shù)化類型等燎字。因此腥椒,JCP組織對(duì)虛擬機(jī)規(guī)范做出了相應(yīng)的修改阿宅,引入了諸如Signature、LocalVariableTypeTable等新的屬性用于解決伴隨泛型而來(lái)的參數(shù)類型的識(shí)別問(wèn)題笼蛛,Signature是其中最重要的一項(xiàng)屬性洒放,它的作用就是存儲(chǔ)一個(gè)方法在字節(jié)碼層面的特征簽名[3],這個(gè)屬性中保存的參數(shù)類型并不是原生類型滨砍,而是包括了參數(shù)化類型的信息往湿。修改后的虛擬機(jī)規(guī)范要求所有能識(shí)別49.0以上版本的Class文件的虛擬機(jī)都要能正確地識(shí)別Signature參數(shù)。
另外惋戏,從Signature屬性的出現(xiàn)我們還可以得出結(jié)論领追,擦除法所謂的擦除,僅僅是對(duì)方法的Code屬性中的字節(jié)碼進(jìn)行擦除响逢,實(shí)際上元數(shù)據(jù)中還是保留了泛型信息绒窑,這也是我們能通過(guò)反射手段取得參數(shù)化類型的根本依據(jù)。