最近準(zhǔn)備回歸下基礎(chǔ)知識,先對泛型進(jìn)行下總結(jié),從以下幾個方面進(jìn)行闡述:
- 泛型的引入及工作原理
- 泛型注意事項及帶來的問題
- 泛型的通配符相關(guān)
1. 泛型的引入及工作原理
先來說說為什么會引入泛型,泛型是jdk1.5引入的。在jdk1.5以前,如果要實(shí)現(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();//需要做類型強(qiáng)轉(zhuǎn)
String c=(String)a.getB();//運(yùn)行時,ClassCastException
編譯器檢查不出這種錯誤,只有在運(yùn)行期才能檢查出來,此時就會出現(xiàn)惱人的ClassCastException,應(yīng)用當(dāng)然也就掛了。所以用Object來實(shí)現(xiàn)泛型的功能就要求時刻做好類型轉(zhuǎn)換,很容易出現(xiàn)問題。那么有沒有辦法將這些檢查放在編譯期做呢,泛型就產(chǎn)生了,泛型在編譯期進(jìn)行類型檢查,問題就容易發(fā)現(xiàn)的多了。我們用泛型來實(shí)現(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();//不需要做類型強(qiáng)轉(zhuǎn),自動完成
String c=(String)a.getB();//編譯期報錯,直接編譯不通過
顯而易見,泛型的出現(xiàn)減少了很多強(qiáng)轉(zhuǎn)的操作,同時避免了很多運(yùn)行時的錯誤,在編譯期完成檢查。
泛型工作原理
java中的泛型都是編譯器層面來完成的,在生成的java字節(jié)碼中是不包含任何泛型中的類型信息的,使用泛型時加上的類型參數(shù),會在編譯時被編譯器去掉。
這個過程稱為類型擦除。泛型是通過類型擦除來實(shí)現(xiàn)的,編譯器在編譯時擦除了所有泛型類型相關(guān)的信息,所以在運(yùn)行時不存在任何泛型類型相關(guān)的信息(暫且這么說,實(shí)際上并不是完全擦除),譬如 List<Integer> 在運(yùn)行時僅用一個 List 來表示,這樣做的目的是為了和 Java 1.5 之前版本進(jìn)行兼容。泛型擦除具體來說就是在編譯成字節(jié)碼時首先進(jìn)行類型檢查,接著進(jìn)行類型擦除(即所有類型參數(shù)都用他們的限定類型替換,包括類、變量和方法),下面來看幾個關(guān)于擦除原理的相關(guān)問題,加深一下理解。
- 上文中我們在調(diào)用getB方法時不需要手動做類型強(qiáng)轉(zhuǎn),其實(shí)并不是不需要,而是編譯器給我們進(jìn)行了處理,具體來講,泛型方法的返回類型是被擦除了,并不會進(jìn)行強(qiáng)轉(zhuǎn),而是在調(diào)用方法的地方插入了強(qiáng)制類型轉(zhuǎn)換,下面看一下a.getB()的字節(jié)碼。用javap查看下上面代碼的字節(jié)碼。
//定義處已經(jīng)被擦出成Object,無法進(jìn)行強(qiáng)轉(zhuǎn),不知道強(qiáng)轉(zhuǎn)成什么
public T getB();
Code:
0: aload_0
1: getfield #23 // Field b:Ljava/lang/Object;
4: areturn
//調(diào)用處利用checkcast進(jìn)行強(qiáng)轉(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>()是不合法的,因?yàn)轭愋筒脸髸鎿Q成Object(如果通過extends設(shè)置了上限,則替換成上限類型),int顯然無法替換成Object,所以泛型參數(shù)必須是引用類型。
泛型擦除會導(dǎo)致任何在運(yùn)行時需要知道確切類型信息的操作都無法編譯通過。例如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())時編譯不通過,說明確實(shí)實(shí)現(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只是個假象,虛擬機(jī)巧妙使用橋方法的方式,解決了類型擦除和多態(tài)的沖突。這里同時存在兩個getValue()方法,getValue:()Ljava/lang/Number和getValue:()Ljava/lang/Object。如果是我們自己編寫的java源代碼,是通不過編譯器的檢查的。這里需要介紹幾個概念。描述符和特征簽名,這里只針對method,不關(guān)心field。
描述符是針對java虛擬機(jī)層面的概念,是針對class文件字節(jié)碼定義的,方法描述符是包括返回值的。
A method descriptor represents the parameters that the method takes and the value that it returns:
特征簽名的概念就不一樣了,java語言規(guī)范和java虛擬機(jī)規(guī)范中存在不同的定義。
java語言層面的方法特征簽名可以表述為:
特征簽名 = 方法名 + 參數(shù)類型 + 參數(shù)順序;
JVM層面的方法特征簽名可以表述為:
特征簽名 = 方法名 + 參數(shù)類型 + 參數(shù)順序 + 返回值類型;
如果存在類型變量或參數(shù)化類型,還包括類型變量或參數(shù)化類型編譯未擦除類型前的信息(FormalTypeParametersopt)和拋出的異常信息(ThrowsSignature),上面的表述可能不太嚴(yán)謹(jǐn),不同的jvm版本是有變更的。
這就解釋了為什么編譯器加入了橋方法后能夠正常運(yùn)行,我們加入?yún)s不行的問題。換句話說class文件結(jié)構(gòu)是允許返回值不同的兩個方法共存的,是符合class文件規(guī)范的。在熱修復(fù)領(lǐng)域,橋方法的使用有時會給泛型方法修復(fù)帶來很多麻煩,這里就不多說了,感興趣的可以閱讀美團(tuán)的這篇文章Android熱更新方案Robust開源,新增自動化補(bǔ)丁工具。
進(jìn)一步想一下,泛型類型擦除到底都擦除了哪些信息,是全部擦除嗎?
其實(shí)java虛擬機(jī)規(guī)范中為了響應(yīng)在泛型類中如何獲取傳入的參數(shù)化類型等問題,引入了signature,LocalVariableTypeTable等新的屬性來記錄泛型信息,所以所謂的泛型類型擦除,僅僅是對方法的code屬性中的字節(jié)碼進(jìn)行擦除,而原數(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)在就明白了泛型擦除不是擦除全部,不然理解的就太狹隘了。其實(shí),jdk提供了方法來讀取泛型信息的,利用class類的get
GenericSuperClass()方法我們可以在泛型類中去獲取具體傳入?yún)?shù)的類型,本質(zhì)上就是通過signature和LocalVariableTypeTable來獲取的。我們可以利用這些虛擬機(jī)給我們保留的泛型信息做哪些事呢?
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進(jìn)行調(diào)用,結(jié)果在抽象類中動態(tài)的獲取到了Person類型。jdk提供的api的使用基本上像getType方法所示。我們想想其實(shí)序列化的工具就是將json數(shù)據(jù)序列化為clazz對象,前提就是要傳入Type的類型,這時候Type的類型獲取就很重要了,我們完全可以在泛型抽象類里面來完成所有的類型獲取、json序列化等工作,有些網(wǎng)絡(luò)請求框架就是這么處理的,這也是在實(shí)際工作場景的應(yīng)用。
好了,泛型引入帶來的問題介紹的差不多了,最后說一下泛型的通配符。
3. 泛型的通配符
泛型中的通配符一般分為非限定通配符和限定通配符兩種,限定通配符有兩種: <? extends T>和 <? super T>。<? extends T> 保證泛型類型必須是 T 的子類來設(shè)定泛型類型的上邊界,<? super T> 來保證泛型類型必須是 T 的父類來設(shè)定類型的下邊界,泛型類型必須用限定內(nèi)的類型來進(jìn)行初始化,否則會導(dǎo)致編譯錯誤。非限定通配符指的是<?>這種形式,可以用任意泛型類型來代替,因?yàn)榉盒褪遣恢С掷^承關(guān)系的,所以<?>很大程度上彌補(bǔ)了這一不足。說一個簡單的例子來體驗(yàn)下?的作用。
比如說現(xiàn)在有兩個List,一個是List<Integer>,一個是List<String>,我想用一個方法打印下list里面的值,因?yàn)榉盒褪菬o法繼承的,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,成功運(yùn)行了,那如果我想把第一個元素在添加一遍呢,好說直接加一條語句就行了。
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方法,因?yàn)樾螀㈩愋褪俏粗模幾g器無法檢測出來了。所以我們在使用?通配符時一定要注意寫入問題。
簡單總結(jié)一句話:一旦形參中使用了?通配符,那么除了寫入null以外,不可以調(diào)用任何和泛型參數(shù)有關(guān)的方法,當(dāng)然和泛型參數(shù)無關(guān)的方法是可以調(diào)用的。
關(guān)于通配符這一塊,需要具體的實(shí)例來進(jìn)行學(xué)習(xí)比較好,很多種情形許多種坑,我覺得【碼農(nóng)每日一題】Java 泛型邊界通配符基礎(chǔ)面試題和【【碼農(nóng)每日一題】Java 泛型邊界與通配符實(shí)戰(zhàn)踩坑面試題介紹的demo非常好,強(qiáng)烈建議查看,受篇幅原因,這里就不過多介紹了,有興趣的同學(xué)可以查看。
好了,有關(guān)泛型的知識就總結(jié)這么多,有問題歡迎指正。