最近準(zhǔn)備回歸下基礎(chǔ)知識站故,先對泛型進行下總結(jié)椅棺,從以下幾個方面進行闡述:
- 泛型的引入及工作原理
- 泛型注意事項及帶來的問題
- 泛型的通配符相關(guān)
1. 泛型的引入及工作原理
先來說說為什么會引入泛型,泛型是jdk1.5引入的。在jdk1.5以前滥玷,如果要實現(xiàn)類似泛型的功能,基本上都是依賴于Object巍棱。比如:
public class A {
private Object b;
public void setB(Object b) {
this.b = b;
}
public Object getB() {
return b;
}
}
--------------------------------------
A a=new A();
a.setB(1);
int b=(int)a.getB();//需要做類型強轉(zhuǎn)
String c=(String)a.getB();//運行時惑畴,ClassCastException
編譯器檢查不出這種錯誤,只有在運行期才能檢查出來航徙,此時就會出現(xiàn)惱人的ClassCastException如贷,應(yīng)用當(dāng)然也就掛了。所以用Object來實現(xiàn)泛型的功能就要求時刻做好類型轉(zhuǎn)換捉偏,很容易出現(xiàn)問題倒得。那么有沒有辦法將這些檢查放在編譯期做呢,泛型就產(chǎn)生了夭禽,泛型在編譯期進行類型檢查霞掺,問題就容易發(fā)現(xiàn)的多了。我們用泛型來實現(xiàn)一下看看:
public class A<T> {
private T b;
public void setB(T b) {
this.b = b;
}
public T getB() {
return b;
}
}
// Test1.java
A<Integer> a=new A<Integer>();
a.setB(1);
int b=a.getB();//不需要做類型強轉(zhuǎn)讹躯,自動完成
String c=(String)a.getB();//編譯期報錯,直接編譯不通過
顯而易見菩彬,泛型的出現(xiàn)減少了很多強轉(zhuǎn)的操作,同時避免了很多運行時的錯誤潮梯,在編譯期完成檢查骗灶。
泛型工作原理
java中的泛型都是編譯器層面來完成的,在生成的java字節(jié)碼中是不包含任何泛型中的類型信息的秉馏,使用泛型時加上的類型參數(shù)耙旦,會在編譯時被編譯器去掉。
這個過程稱為類型擦除萝究。泛型是通過類型擦除來實現(xiàn)的免都,編譯器在編譯時擦除了所有泛型類型相關(guān)的信息,所以在運行時不存在任何泛型類型相關(guān)的信息(暫且這么說帆竹,實際上并不是完全擦除)绕娘,譬如 List<Integer> 在運行時僅用一個 List 來表示,這樣做的目的是為了和 Java 1.5 之前版本進行兼容栽连。泛型擦除具體來說就是在編譯成字節(jié)碼時首先進行類型檢查险领,接著進行類型擦除(即所有類型參數(shù)都用他們的限定類型替換侨舆,包括類、變量和方法)绢陌,下面來看幾個關(guān)于擦除原理的相關(guān)問題,加深一下理解挨下。
- 上文中我們在調(diào)用getB方法時不需要手動做類型強轉(zhuǎn),其實并不是不需要下面,而是編譯器給我們進行了處理复颈,具體來講,泛型方法的返回類型是被擦除了沥割,并不會進行強轉(zhuǎn)耗啦,而是在調(diào)用方法的地方插入了強制類型轉(zhuǎn)換,下面看一下a.getB()的字節(jié)碼机杜。用javap查看下上面代碼的字節(jié)碼帜讲。
//定義處已經(jīng)被擦出成Object,無法進行強轉(zhuǎn)椒拗,不知道強轉(zhuǎn)成什么
public T getB();
Code:
0: aload_0
1: getfield #23 // Field b:Ljava/lang/Object;
4: areturn
//調(diào)用處利用checkcast進行強轉(zhuǎn)
L5 {
aload1
invokevirtual com/ljj/A getB()Ljava.lang.Object);
checkcast java/lang/Integer
invokevirtual java/lang/Integer intValue(()I);
istore2
}
2. 泛型注意事項及帶來的問題
泛型類型參數(shù)不能是基本類型似将。例如我們直接使用new ArrayList<int>()是不合法的,因為類型擦除后會替換成Object(如果通過extends設(shè)置了上限蚀苛,則替換成上限類型)在验,int顯然無法替換成Object,所以泛型參數(shù)必須是引用類型堵未。
泛型擦除會導(dǎo)致任何在運行時需要知道確切類型信息的操作都無法編譯通過腋舌。例如test1,test2渗蟹,test3都無法編譯通過块饺,這里說明下,instanceof語句是不可以直接用于泛型比較的雌芽,上文代碼中授艰,a instanceof A<integer>不可以,但是a instanceof A或者 a instanceof A<?>都是沒有問題的世落,只是具體的泛型類型不可以使用instanceof淮腾。
public class A<T> {
private void test1(Object arg) {
if (arg instanceof T) { // 編譯不通過
}
}
private void test2() {// 編譯不通過
T obj = new T();
}
private void test3() {// 編譯不通過
T[] vars = new T[10];
}
}
- 類型擦除與多態(tài)的沖突,我們通過下面的例子來引入屉佳。
class A<T> {
private T value;
public void setValue(T t) {
this.value = t;
}
public T getValue() {
return value;
}
}
class ASub extends A<Number> {
@Override // 與父類參數(shù)不一樣来破,為什么用@Override修飾
public void setValue(Number t) {
super.setValue(t);
}
@Override // 與父類返回值不一樣,為什么用@Override修飾
public Number getValue() {
return super.getValue();
}
}
ASub aSub=new ASub();
aSub.setValue(123);//編譯成功
aSub.setValue(new Object);//編譯不通過
不知道大家看完這段代碼后忘古,有沒有比較詫異?按照前面類型擦除的原理诅诱,為什么ASub的setValue和getValue方法都可以用@Override修飾能不報錯髓堪?
我們知道@Override修飾的代表重寫,重寫要求子類中的方法與父類中的某一方法具有相同的方法名,返回類型和參數(shù)列表干旁。顯然子類的getValue方法的會返回值與父類不同驶沼。而setValue方法就更奇怪了,A方法的setValue方法在類型擦除后應(yīng)該是setValue(Object obj)争群,看起來這不是重寫回怜,不就是我們認(rèn)知中的重載(函數(shù)名相同,參數(shù)不同)嗎换薄?而且最后當(dāng)我們調(diào)用aSub.setValue(new Object())時編譯不通過玉雾,說明確實實現(xiàn)了重寫功能,而非重載轻要。我們看一下通過javap編譯后的class文件复旬。
Compiled from "ASub.java"
public class com.ljj.ASub extends com.ljj.A<java.lang.Number> {
public com.ljj.ASub();
Code:
0: aload_0
1: invokespecial #8 // Method com/ljj/A."<init>":()V
4: return
public void setValue(java.lang.Number);
Code:
0: aload_0
1: aload_1
2: invokespecial #16 // Method com/ljj/A.setValue:(Ljava/lang/Object;)V
5: return
public java.lang.Number getValue();
Code:
0: aload_0
1: invokespecial #23 // Method com/ljj/A.getValue:()Ljava/lang/Object;
4: checkcast #26 // class java/lang/Number
7: areturn
public void setValue(java.lang.Object);//編譯器生成的橋方法,調(diào)用重寫的setValue方法
Code:
0: aload_0
1: aload_1
2: checkcast #26 // class java/lang/Number
5: invokevirtual #28 // Method setValue:(Ljava/lang/Number;)V
8: return
public java.lang.Object getValue();//編譯器生成的橋方法冲泥,調(diào)用重寫的getValue方法
Code:
0: aload_0
1: invokevirtual #30 // Method getValue:()Ljava/lang/Number;
4: areturn
}
我們可以看到子類真正重寫基類方法的是編譯器自動合成的橋方法驹碍,而橋方法的內(nèi)部直接去調(diào)用了我們復(fù)寫的方法,可見凡恍,加載getValue和setValue上的@Override只是個假象志秃,虛擬機巧妙使用橋方法的方式,解決了類型擦除和多態(tài)的沖突嚼酝。這里同時存在兩個getValue()方法浮还,getValue:()Ljava/lang/Number和getValue:()Ljava/lang/Object。如果是我們自己編寫的java源代碼革半,是通不過編譯器的檢查的碑定。這里需要介紹幾個概念。描述符和特征簽名又官,這里只針對method延刘,不關(guān)心field。
描述符是針對java虛擬機層面的概念六敬,是針對class文件字節(jié)碼定義的碘赖,方法描述符是包括返回值的。
A method descriptor represents the parameters that the method takes and the value that it returns:
特征簽名的概念就不一樣了外构,java語言規(guī)范和java虛擬機規(guī)范中存在不同的定義普泡。
java語言層面的方法特征簽名可以表述為:
特征簽名 = 方法名 + 參數(shù)類型 + 參數(shù)順序;
JVM層面的方法特征簽名可以表述為:
特征簽名 = 方法名 + 參數(shù)類型 + 參數(shù)順序 + 返回值類型审编;
如果存在類型變量或參數(shù)化類型撼班,還包括類型變量或參數(shù)化類型編譯未擦除類型前的信息(FormalTypeParametersopt)和拋出的異常信息(ThrowsSignature),上面的表述可能不太嚴(yán)謹(jǐn)垒酬,不同的jvm版本是有變更的砰嘁。
這就解釋了為什么編譯器加入了橋方法后能夠正常運行件炉,我們加入?yún)s不行的問題。換句話說class文件結(jié)構(gòu)是允許返回值不同的兩個方法共存的矮湘,是符合class文件規(guī)范的斟冕。在熱修復(fù)領(lǐng)域,橋方法的使用有時會給泛型方法修復(fù)帶來很多麻煩缅阳,這里就不多說了磕蛇,感興趣的可以閱讀美團的這篇文章Android熱更新方案Robust開源,新增自動化補丁工具十办。
進一步想一下秀撇,泛型類型擦除到底都擦除了哪些信息,是全部擦除嗎橘洞?
其實java虛擬機規(guī)范中為了響應(yīng)在泛型類中如何獲取傳入的參數(shù)化類型等問題捌袜,引入了signature,LocalVariableTypeTable等新的屬性來記錄泛型信息炸枣,所以所謂的泛型類型擦除虏等,僅僅是對方法的code屬性中的字節(jié)碼進行擦除,而原數(shù)據(jù)中還是保留了泛型信息的适肠,這些信息被保存在class字節(jié)碼的常量池中霍衫,使用了泛型的代碼調(diào)用處會生成一個signature簽名字段,signature指明了這個常量在常量池的地址侯养,這樣我們就找到了參數(shù)化類型敦跌,空口無憑,我們寫個非常簡單的demo看一下逛揩,沒法再簡單了柠傍,我們只寫了兩個函數(shù),第一個函數(shù)入?yún)盒捅缁诙€方法入?yún)⒅皇莝tring惧笛。
public class Test2 {
public static void mytest(List<Integer> s) {
}
public static void mytest(String s) {
}
}
我們利用javap工具看一下,注意此時要看詳細(xì)的反匯編信息逞泄,要添加-c參數(shù)患整。
Constant pool:
#14 = Utf8 mytest
#15 = Utf8 (Ljava/util/List;)V
#16 = Utf8 Signature
#17 = Utf8 (Ljava/util/List<Ljava/lang/Integer;>;)V
一目了然,可以看出來調(diào)用到了泛型的地方會添加signature和LocalVariableTypeTable喷众,現(xiàn)在就明白了泛型擦除不是擦除全部各谚,不然理解的就太狹隘了。其實到千,jdk提供了方法來讀取泛型信息的,利用class類的get
GenericSuperClass()方法我們可以在泛型類中去獲取具體傳入?yún)?shù)的類型昌渤,本質(zhì)上就是通過signature和LocalVariableTypeTable來獲取的。我們可以利用這些虛擬機給我們保留的泛型信息做哪些事呢憔四?
public abstract class AbstractHandler<T> {
T obj;
public abstract void onSuccess(Class<T> clazz);
public void handle() {
onSuccess(getType());
}
private Class<T> getType() {
Class<T> entityClass = null;
Type t = getClass().getGenericSuperclass();
if (t instanceof ParameterizedType) {
Type[] p = ((ParameterizedType) t).getActualTypeArguments();
entityClass = (Class<T>) p[0];
}
return entityClass;
}
}
-------------------------------------------------
public class Test1 {
public static void main(String[] args) {
new AbstractHandler<Person>() {
@Override
public void onSuccess(Class<Person> clazz) {
System.out.println(clazz);
}
}.handle();
}
static class Person {
String name;
}
}
------------------------------
輸出結(jié)果:class com.ljj.Test1$Person
我們來簡單的分析下這段代碼愈涩,定義一個抽象類AbstractHandler望抽,提供一個回調(diào)方法onSuccess方法。然后通過一個匿名子類傳入一個Person進行調(diào)用履婉,結(jié)果在抽象類中動態(tài)的獲取到了Person類型。jdk提供的api的使用基本上像getType方法所示斟览。我們想想其實序列化的工具就是將json數(shù)據(jù)序列化為clazz對象毁腿,前提就是要傳入Type的類型,這時候Type的類型獲取就很重要了苛茂,我們完全可以在泛型抽象類里面來完成所有的類型獲取已烤、json序列化等工作,有些網(wǎng)絡(luò)請求框架就是這么處理的妓羊,這也是在實際工作場景的應(yīng)用胯究。
好了,泛型引入帶來的問題介紹的差不多了躁绸,最后說一下泛型的通配符裕循。
3. 泛型的通配符
泛型中的通配符一般分為非限定通配符和限定通配符兩種,限定通配符有兩種: <? extends T>和 <? super T>净刮。<? extends T> 保證泛型類型必須是 T 的子類來設(shè)定泛型類型的上邊界剥哑,<? super T> 來保證泛型類型必須是 T 的父類來設(shè)定類型的下邊界,泛型類型必須用限定內(nèi)的類型來進行初始化淹父,否則會導(dǎo)致編譯錯誤株婴。非限定通配符指的是<?>這種形式,可以用任意泛型類型來代替暑认,因為泛型是不支持繼承關(guān)系的困介,所以<?>很大程度上彌補了這一不足。說一個簡單的例子來體驗下蘸际?的作用座哩。
比如說現(xiàn)在有兩個List,一個是List<Integer>,一個是List<String>,我想用一個方法打印下list里面的值捡鱼,因為泛型是無法繼承的八回,List<Integer>和List<Object>是沒有關(guān)系的,我們此時可以借助于通配符解決驾诈。
public class Test1 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
list.add(12);
handle(list);
List<Float> list1 = new ArrayList<Float>();
list1.add(123.0f);
handle(list1);
}
private static void handle(List<?> list) {
System.out.println(list.get(0));
}
}
ok,成功運行了缠诅,那如果我想把第一個元素在添加一遍呢,好說直接加一條語句就行了乍迄。
private static void handle(List<?> list){
System.out.println(list.get(0));
list.add(list.get(0));
}
此時管引,你會發(fā)現(xiàn)編譯不過去了。
The method add(capture#2-of ?) in the type List<capture#2-of ?> is not applicable for the arguments (capture#3-of ?)闯两。
“capture#2 of ?” 表示什么褥伴?當(dāng)編譯器遇到一個在其類型中帶有通配符的變量谅将,它認(rèn)識到必然有一些 T ,它不知道 T 代表什么類型重慢,但它可以為該類型創(chuàng)建一個占位符來指代 T 的類型饥臂。占位符被稱為這個特殊通配符的捕獲(capture)。這種情況下似踱,編譯器將名稱 “capture#2of ?” 以 List類型分配給通配符隅熙,每個變量聲明中每出現(xiàn)一個通配符都將獲得一個不同的捕獲,錯誤消息告訴我們不能調(diào)用add方法核芽,因為形參類型是未知的囚戚,編譯器無法檢測出來了。所以我們在使用轧简?通配符時一定要注意寫入問題驰坊。
簡單總結(jié)一句話:一旦形參中使用了?通配符哮独,那么除了寫入null以外拳芙,不可以調(diào)用任何和泛型參數(shù)有關(guān)的方法,當(dāng)然和泛型參數(shù)無關(guān)的方法是可以調(diào)用的借嗽。
關(guān)于通配符這一塊态鳖,需要具體的實例來進行學(xué)習(xí)比較好,很多種情形許多種坑恶导,我覺得【碼農(nóng)每日一題】Java 泛型邊界通配符基礎(chǔ)面試題和【【碼農(nóng)每日一題】Java 泛型邊界與通配符實戰(zhàn)踩坑面試題介紹的demo非常好浆竭,強烈建議查看,受篇幅原因惨寿,這里就不過多介紹了邦泄,有興趣的同學(xué)可以查看。
好了裂垦,有關(guān)泛型的知識就總結(jié)這么多顺囊,有問題歡迎指正。