泛型常用特點(diǎn)

首先,Java有泛型這一個(gè)概念,初衷是為了保證在運(yùn)行時(shí)出現(xiàn)的錯(cuò)誤能提早放到編譯時(shí)檢查。

1)Java泛型

開發(fā)人員在使用泛型的時(shí)候,很容易根據(jù)自己的直覺而犯一些錯(cuò)誤。比如一個(gè)方法如果接收List<Object>作為形式參數(shù),那么如果嘗試將一個(gè)List<String>的對(duì)象作為實(shí)際參數(shù)傳進(jìn)去,卻發(fā)現(xiàn)無法通過編譯。雖然從直覺上來說,Object是String的父類,這種類型轉(zhuǎn)換應(yīng)該是合理的。但是實(shí)際上這會(huì)產(chǎn)生隱含的類型轉(zhuǎn)換問題,因此編譯器直接就禁止這樣的行為。

2)實(shí)現(xiàn)方式:類型擦除

Java中的泛型基本上都是在編譯器這個(gè)層次來實(shí)現(xiàn)的,在生成的Java字節(jié)代碼中是不包含泛型中的類型信息的。

使用泛型的時(shí)候加上的類型參數(shù),會(huì)被編譯器在編譯的時(shí)候去掉,這個(gè)過程就稱為類型擦除。

如在代碼中定義的List<Object>和List<String>等類型,在編譯之后都會(huì)變成List。JVM看到的只是List,而由泛型附加的類型信息對(duì)JVM來說是不可見的。Java編譯器會(huì)在編譯時(shí)盡可能的發(fā)現(xiàn)可能出錯(cuò)的地方,但是仍然無法避免在運(yùn)行時(shí)刻出現(xiàn)類型轉(zhuǎn)換異常的情況。

很多泛型的奇怪特性都與這個(gè)類型擦除的存在有關(guān),包括:

  1. 泛型類并沒有自己獨(dú)有的Class類對(duì)象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class。

  2. 靜態(tài)變量是被泛型類的所有實(shí)例所共享的。對(duì)于聲明為MyClass<T>的類,訪問其中的靜態(tài)變量的方法仍然是 MyClass.myStaticVar。不管是通過new MyClass<String>還是new MyClass<Integer>創(chuàng)建的對(duì)象,都是共享一個(gè)靜態(tài)變量。

  3. 泛型的類型參數(shù)不能用在Java異常處理的catch語句中。因?yàn)楫惓L幚硎怯蒍VM在運(yùn)行時(shí)刻來進(jìn)行的。由于類型信息被擦除,JVM是無法區(qū)分兩個(gè)異常類型MyException<String>和MyException<Integer>的。對(duì)于JVM來說,它們都是 MyException類型的。也就無法執(zhí)行與異常對(duì)應(yīng)的catch語句。

類型擦除的基本過程也比較簡(jiǎn)單,首先是找到用來替換類型參數(shù)的具體類。這個(gè)具體類一般是Object。如果指定了類型參數(shù)的上界的話,則使用這個(gè)上界。把代碼中的類型參數(shù)都替換成具體的類。同時(shí)去掉出現(xiàn)的類型聲明,即去掉<>的內(nèi)容。比如T get()方法聲明就變成了Object get();List<String>就變成了List。接下來就可能需要生成一些橋接方法(bridge method)。這是由于擦除了類型之后的類可能缺少某些必須的方法。比如考慮下面的代碼:

  class MyString implements Comparable<String> {
      public int compareTo(String str) {        
          return 0;    
      }
  }

當(dāng)類型信息被擦除之后,上述類的聲明變成了
class MyString implements Comparable
但是這樣的話,類MyString就會(huì)有編譯錯(cuò)誤,因?yàn)闆]有實(shí)現(xiàn)接口Comparable聲明的int compareTo(Object)方法。這個(gè)時(shí)候就由編譯器來動(dòng)態(tài)生成這個(gè)方法。

3)泛型的檢測(cè):不符合泛型T的檢測(cè)

在進(jìn)行編譯之前就對(duì)所有泛型進(jìn)行檢測(cè),加入類型檢測(cè)和轉(zhuǎn)換的指令,比如返回泛型的結(jié)果實(shí)際上返回的是擦出后的類型,而虛擬機(jī)會(huì)多加一個(gè)類型轉(zhuǎn)換的指令。

4)需要泛型之間的類型轉(zhuǎn)換怎么做?通配符?與PECS

  1. 如果你是想遍歷collection,并對(duì)每一項(xiàng)元素操作時(shí),此時(shí)這個(gè)集合時(shí)生產(chǎn)者(生產(chǎn)元素),應(yīng)該使用 Collection<? extends E>,因?yàn)橄鄬?duì)于你泛型E,能放進(jìn)E中的(把Collection中的元素放入E類型中) ,應(yīng)該是E的子類型。

  2. 如果你是想添加元素到collection中去,那么此時(shí)集合時(shí)消費(fèi)者(消費(fèi)元素)應(yīng)該使用Collection<? super E>,同理,你要將E類的元素放入Collection中去,那么Collection應(yīng)該存放的是E的父類型。

5)泛型與多態(tài)的沖突

現(xiàn)在有這樣一個(gè)泛型類:

class Pair<T> {
    private T value;
    public T getValue() {
        return value;
    }
    public void setValue(T value) {
        this.value = value;
    }
}

然后我們想要一個(gè)子類繼承它


class DateInter extends Pair<Date> {
    @Override
    public void setValue(Date value) {
        super.setValue(value);
    }
    @Override
    public Date getValue() {
        return super.getValue();
    }

在這個(gè)子類中,我們?cè)O(shè)定父類的泛型類型為Pair<Date>,在子類中,我們覆蓋了父類的兩個(gè)方法,我們的原意是這樣的:
將父類的泛型類型限定為Date,那么父類里面的兩個(gè)方法的參數(shù)都為Date類型:

public Date getValue() {
        return value;
    }
    public void setValue(Date value) {
        this.value = value;
    }

所以,我們?cè)谧宇愔兄貙戇@兩個(gè)方法一點(diǎn)問題也沒有,實(shí)際上,從他們的@Override標(biāo)簽中也可以看到,一點(diǎn)問題也沒有,實(shí)際上是這樣的嗎?

分析:

實(shí)際上,類型擦除后,父類的的泛型類型全部變?yōu)榱嗽碱愋蚈bject,所以父類編譯之后會(huì)變成下面的樣子:

class Pair {
    private Object value;
    public Object getValue() {
        return value;
    }
    public void setValue(Object  value) {
        this.value = value;
    }
}

再看子類的兩個(gè)重寫的方法的類型:

@Override
    public void setValue(Date value) {
        super.setValue(value);
    }
    @Override
    public Date getValue() {
        return super.getValue();
    }

先來分析setValue方法,父類的類型是Object,而子類的類型是Date,參數(shù)類型不一樣,這如果實(shí)在普通的繼承關(guān)系中,根本就不會(huì)是重寫,而是重載。
我們?cè)谝粋€(gè)main方法測(cè)試一下:

public static void main(String[] args) throws ClassNotFoundException {
        DateInter dateInter=new DateInter();
        dateInter.setValue(new Date());                
        dateInter.setValue(new Object());//編譯錯(cuò)誤
 }

如果是重載,那么子類中兩個(gè)setValue方法,一個(gè)是參數(shù)Object類型,一個(gè)是Date類型,可是我們發(fā)現(xiàn),根本就沒有這樣的一個(gè)子類繼承自父類的Object類型參數(shù)的方法。所以說,卻是是重寫了,而不是重載了。

為什么會(huì)這樣呢?

原因是這樣的,我們傳入父類的泛型類型是Date,Pair<Date>,我們的本意是將泛型類變?yōu)槿缦拢?/p>

class Pair {
    private Date value;
    public Date getValue() {
        return value;
    }
    public void setValue(Date value) {
        this.value = value;
    }
}

然后再子類中重寫參數(shù)類型為Date的那兩個(gè)方法,實(shí)現(xiàn)繼承中的多態(tài)。
可是由于種種原因,虛擬機(jī)并不能將泛型類型變?yōu)镈ate,只能將類型擦除掉,變?yōu)樵碱愋蚈bject。這樣,我們的本意是進(jìn)行重寫,實(shí)現(xiàn)多態(tài)??墒穷愋筒脸?,只能變?yōu)榱酥剌d。這樣,類型擦除就和多態(tài)有了沖突。JVM知道你的本意嗎?知道?。?!可是它能直接實(shí)現(xiàn)嗎,不能?。?!如果真的不能的話,那我們?cè)趺慈ブ貙懳覀兿胍腄ate類型參數(shù)的方法啊。

于是JVM采用了一個(gè)特殊的方法,來完成這項(xiàng)功能,那就是橋方法。

首先,我們用javap -c className的方式反編譯下DateInter子類的字節(jié)碼,結(jié)果如下:

class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {
  com.tao.test.DateInter();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method com/tao/test/Pair."<init>"
:()V
       4: return
 
  public void setValue(java.util.Date);  //我們重寫的setValue方法
    Code:
       0: aload_0
       1: aload_1
       2: invokespecial #16                 // Method com/tao/test/Pair.setValue
:(Ljava/lang/Object;)V
       5: return
 
  public java.util.Date getValue();    //我們重寫的getValue方法
    Code:
       0: aload_0
       1: invokespecial #23                 // Method com/tao/test/Pair.getValue
:()Ljava/lang/Object;
       4: checkcast     #26                 // class java/util/Date
       7: areturn
 
  public java.lang.Object getValue();     //編譯時(shí)由編譯器生成的巧方法
    Code:
       0: aload_0
       1: invokevirtual #28                 // Method getValue:()Ljava/util/Date 去調(diào)用我們重寫的getValue方法
;
       4: areturn
 
  public void setValue(java.lang.Object);   //編譯時(shí)由編譯器生成的巧方法
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #26                 // class java/util/Date
       5: invokevirtual #30                 // Method setValue:(Ljava/util/Date;   去調(diào)用我們重寫的setValue方法
)V
       8: return
}

從編譯的結(jié)果來看,我們本意重寫setValue和getValue方法的子類,竟然有4個(gè)方法,其實(shí)不用驚奇,最后的兩個(gè)方法,就是編譯器自己生成的橋方法。可以看到橋方法的參數(shù)類型都是Object,也就是說,子類中真正覆蓋父類兩個(gè)方法的就是這兩個(gè)我們看不到的橋方法。而打在我們自己定義的setvalue和getValue方法上面的@Oveerride只不過是假象。而橋方法的內(nèi)部實(shí)現(xiàn),就只是去調(diào)用我們自己重寫的那兩個(gè)方法。
所以,虛擬機(jī)巧妙的使用了巧方法,來解決了類型擦除和多態(tài)的沖突。

不過,要提到一點(diǎn),這里面的setValue和getValue這兩個(gè)橋方法的意義又有不同。

setValue方法是為了解決類型擦除與多態(tài)之間的沖突。

而getValue卻有普遍的意義,怎么說呢,如果這是一個(gè)普通的繼承關(guān)系:

那么父類的setValue方法如下:

public Object getValue() {
        return super.getValue();
    }

而子類重寫的方法是:

public Date getValue() {
        return super.getValue();
    }

其實(shí)這在普通的類繼承中也是普遍存在的重寫,這就是協(xié)變。
關(guān)于協(xié)變:。。。。。。

并且,還有一點(diǎn)也許會(huì)有疑問,子類中的巧方法 Object getValue()和Date getValue()是同 時(shí)存在的,可是如果是常規(guī)的兩個(gè)方法,他們的方法簽名是一樣的,也就是說虛擬機(jī)根本不能分別這兩個(gè)方法。如果是我們自己編寫Java代碼,這樣的代碼是無法通過編譯器的檢查的,但是虛擬機(jī)卻是允許這樣做的,因?yàn)樘摂M機(jī)通過參數(shù)類型和返回類型來確定一個(gè)方法,所以編譯器為了實(shí)現(xiàn)泛型的多態(tài)允許自己做這個(gè)看起來“不合法”的事情,然后交給虛擬器去區(qū)別。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容