java泛型你需要知道的一切

最近準(zhǔn)備回歸下基礎(chǔ)知識,先對泛型進(jìn)行下總結(jié),從以下幾個方面進(jìn)行闡述:

  1. 泛型的引入及工作原理
  2. 泛型注意事項及帶來的問題
  3. 泛型的通配符相關(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
test2.png

一目了然,可以看出來調(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é)這么多,有問題歡迎指正。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內(nèi)容

  • ??在Effective中講到泛型之處提到了一個概念,類型擦除器,這是什么呢?接下來我們跟隨這篇文章探索類型擦除的...
    凌云_00閱讀 2,150評論 0 8
  • 在之前的文章中分析過了多態(tài),可以知道多態(tài)本身是一種泛化機(jī)制,它通過基類或者接口來設(shè)計,使程序擁有一定的靈活性,但是...
    _小二_閱讀 691評論 0 0
  • 參數(shù)類型的好處 在 Java 引入泛型之前,泛型程序設(shè)計是用繼承實(shí)現(xiàn)的。ArrayList 類只維護(hù)一個 Obje...
    杰哥長得帥閱讀 894評論 0 3
  • 開發(fā)人員在使用泛型的時候,很容易根據(jù)自己的直覺而犯一些錯誤。比如一個方法如果接收List作為形式參數(shù),那么如果嘗試...
    時待吾閱讀 1,064評論 0 3
  • 我在這里等你,你在哪里呢? ...
    小偶閱讀 475評論 0 0