【Java面試題】之泛型相關

JacobGo發表於2017-10-28

本文轉載自:http://www.cnblogs.com/huajiezh/p/6411123.html


1、泛型基本概念

1.1、由來

泛型是JDK 1.5的一項新特性,在Java語言處於還沒有出現泛型的版本時,只能通過Object是所有型別的父類和型別強制轉換兩個特點的配合來實現型別泛化。例如在雜湊表的存取中,JDK 1.5之前使用HashMap的get()方法,返回值就是一個Object物件,由於Java語言裡面所有的型別都繼承於java.lang.Object,那Object轉型為任何物件成都是有可能的。但是也因為有無限的可能性,就只有程式設計師和執行期的虛擬機器才知道這個Object到底是個什麼型別的物件。在編譯期間,編譯器無法檢查這個Object的強制轉型是否成功,如果僅僅依賴程式設計師去保障這項操作的正確性,許多ClassCastException的風險就會被轉嫁到程式執行期之中。

1.2、偽泛型

泛型技術在C#和Java之中的使用方式看似相同,但實現上卻有著根本性的分歧,C#裡面泛型無論在程式原始碼中、編譯後的IL中(Intermediate Language,中間語言,這時候泛型是一個佔位符)或是執行期的CLR中都是切實存在的,List<Integer>與List<String>就是兩個不同的型別,它們在系統執行期生成,有自己的虛方法表和型別資料,這種實現稱為型別膨脹,基於這種方法實現的泛型被稱為真實泛型。

  Java語言中的泛型則不一樣,它只在程式原始碼中存在,在編譯後的位元組碼檔案中,就已經被替換為原來的原始型別(Raw Type,也稱為裸型別)了,並且在相應的地方插入了強制轉型程式碼,因此對於執行期的Java語言來說,ArrayList<Integer>ArrayList<String>就是同一個類。所以說泛型技術實際上是Java語言的一顆語法糖,Java語言中的泛型實現方法稱為型別擦除,基於這種方法實現的泛型被稱為偽泛型。

1.3、泛型的使用

  • 泛型類

  • 泛型介面


  • 泛型方法

1.4、一些要求及規則

  • 不能使基本型別
  • 不管該限定是類還是介面,統一都使用關鍵字extends
  • 可以使用&符號給出多個限定
  •  如果限定既有介面也有類,那麼類必須只有一個,並且放在首位置

詳見:http://blog.csdn.net/lonelyroamer/article/details/7864531

2、原理

2.1、型別擦除

Java中的泛型基本上都是在編譯器這個層次來實現的。在生成的Java位元組碼中是不包含泛型中的型別資訊的。使用泛型的時候加上的型別引數,會在編譯器在編譯的時候去掉。這個過程就稱為型別擦除。

如在程式碼中定義的List<object>和List<String>等型別,在編譯後都會程式設計List。JVM看到的只是List,而由泛型附加的型別資訊對JVM來說是不可見的。

2.2、重要:用反射來看泛型的機制(甚至可以破壞)

在程式中定義了一個ArrayList泛型型別例項化為Integer的物件,如果直接呼叫add方法,那麼只能儲存整形的資料。不過當我們利用反射呼叫add方法的時候,卻可以儲存字串。這說明了Integer泛型例項在編譯之後被擦除了,只保留了原始型別。

2.3、原始型別

原始型別(raw type)就是擦除去了泛型資訊,最後在位元組碼中的型別變數的真正型別。無論何時定義一個泛型型別,相應的原始型別都會被自動地提供。型別變數被擦除(crased),並使用其限定型別(使用extends的,如果extends多個則原始型別就用第一個邊界的型別變數來替換)替換,無限定的變數用Object替換。

3、型別擦除引起的問題及解決辦法

3.1、先檢查、再編譯

因為型別擦除是在編譯期完成的,在執行的時候就會忽略泛型,為了保證在執行的時候不出現型別錯誤,就需要在編譯之前就檢查是否滿足泛型要求(型別檢查)。

3.2、型別檢查的依據

以上兩種情況都沒有錯誤:第一種情況,在使用arrayList1的時候與完全使用泛型引數一樣的效果,因為new ArrayList()只是在記憶體中新開闢一個儲存空間,它並不能判斷型別,而真正涉及型別檢查的是它的引用,所以在呼叫arrayList1的時候會進行型別檢查。同理,第二種情況,就不會進行型別檢查。

3.3、泛型引數化型別沒有繼承關係(只能使用萬用字元型別)

  • 第一種情況,可以擴充套件為一下形式:

假設它編譯沒錯,那麼當我們使用arrayList2引用用get()方法取值的時候,返回的都是String型別的物件(型別檢測是根據引用來決定的),可是它裡面實際上已經被我們存放了Object型別的物件,這樣就會出現ClassCastException

  • 第二種情況,同樣擴充套件為:

雖然不會出現ClassCastException,但是假設它編譯沒錯,那麼當我們使用arrayList2引用用get()方法取值的時候,返回的都是Object型別的物件,需要進行型別轉換才行,這樣,泛型就完全沒有作用了。

4、型別擦除與多型的衝突和解決方法

現在看來我們在子類中重寫了父類的兩個方法,而實際上,經過型別擦除之後:

可以看到,父類和子類的方法中引數型別不同,所以如果是在普通的繼承關係中,這完全不是重寫,而是過載;但是如果在泛型中呢?

如果是過載,那麼子類中兩個setValue方法,一個是引數Object型別,一個是Date型別,可是我們發現,根本就沒有這樣的一個子類繼承自父類的Object型別引數的方法。所以說,在泛型中確實是重寫了,而不是過載。

4.1、橋方法

         通過編譯原始碼會發現DataInter最後會有四種方法,其中兩個是編譯器自己生成的橋方法,它的引數型別是Object,也就是說,子類中真正覆蓋/重寫父類兩個方法的就是這兩個我們看不到的橋方法。而打在我們自己定義的setvalue和getValue方法只不過是假象。而橋方法的內部實現,就只是去呼叫我們自己重寫的那兩個方法

5、泛型在靜態類和靜態方法中的問題

泛型類中的靜態方法和靜態變數不可以使用泛型類所宣告的泛型型別引數。

因為泛型類中的泛型引數的例項化是在定義物件的時候指定的,而靜態變數和靜態方法不需要使用物件來呼叫。物件都沒有建立,如何確定這個泛型引數是何種型別,所以當然是錯誤的。

因為這是一個泛型方法,在泛型方法中使用的T是自己在方法中定義的T,而不是泛型類中的T。呼叫這個方法時會宣告T型別的。


相關文章