Java中的泛型

版權聲明:本文爲博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/lib739449500/article/details/98597350

目錄

爲什麼我們需要泛型?

泛型類和泛型接口

泛型方法

限定類型變量

泛型中的約束和侷限性

泛型類型的繼承規則

通配符類型

? extends X

? super X

無限定的通配符 ?

虛擬機是如何實現泛型的?


爲什麼我們需要泛型?

通過兩段代碼我們就可以知道爲何我們需要泛型

實際開發中,經常有數值類型求和的需求,例如實現int類型的加法, 有時候還需要實現long類型的求和, 如果還需要double類型的求和,需要重新在重載一個輸入是double類型的add方法。

定義了一個List類型的集合,先向其中加入了兩個字符串類型的值,隨後加入一個Integer類型的值。這是完全允許的,因爲此時list默認的類型爲Object類型。在之後的循環中,由於忘記了之前在list中也加入了Integer類型的值或其他編碼原因,很容易出現類似於//1中的錯誤。因爲編譯階段正常,而運行時會出現“java.lang.ClassCastException”異常。因此,導致此類錯誤編碼過程中不易發現。

 在如上的編碼過程中,我們發現主要存在兩個問題:

1.當我們將一個對象放入集合中,集合不會記住此對象的類型,當再次從集合中取出此對象時,改對象的編譯類型變成了Object類型,但其運行時類型任然爲其本身類型。

2.因此,//1處取出集合元素時需要人爲的強制類型轉化到具體的目標類型,且很容易出現“java.lang.ClassCastException”異常。

所以泛型的好處就是:

  1. 適用於多種數據類型執行相同的代碼
  2. 泛型中的類型在使用時指定,不需要強制類型轉換

泛型類和泛型接口

泛型,即“參數化類型”。一提到參數,最熟悉的就是定義方法時有形參,然後調用此方法時傳遞實參。那麼參數化類型怎麼理解呢?

顧名思義,就是將類型由原來的具體的類型參數化,類似於方法中的變量參數,此時類型也定義成參數形式(可以稱之爲類型形參),然後在使用/調用時傳入具體的類型(類型實參)。

泛型的本質是爲了參數化類型(在不創建新的類型的情況下,通過泛型指定的不同類型來控制形參具體限制的類型)。也就是說在泛型使用過程中,操作的數據類型被指定爲一個參數,這種參數類型可以用在類、接口和方法中,分別被稱爲泛型類、泛型接口、泛型方法。

引入一個類型變量T(其他大寫字母都可以,不過常用的就是T,E,K,V等等),並且用<>括起來,並放在類名的後面。泛型類是允許有多個類型變量的。

 

泛型接口與泛型類的定義基本相同。

而實現泛型接口的類,有兩種實現方法:

1、未傳入泛型實參時:

 

在new出類的實例時,需要指定具體類型:

 

2、傳入泛型實參

 

在new出類的實例時,和普通的類沒區別。

泛型方法

泛型方法,是在調用方法的時候指明泛型的具體類型 ,泛型方法可以在任何地方和任何場景中使用,包括普通類和泛型類。注意泛型類中定義的普通方法和泛型方法的區別。

普通方法:

泛型方法

 

限定類型變量

有時候,我們需要對類型變量加以約束,比如計算兩個變量的最小,最大值。

請問,如果確保傳入的兩個變量一定有compareTo方法?那麼解決這個問題的方案就是將T限制爲實現了接口Comparable的類

T extends Comparable中

T表示應該綁定類型的子類型,Comparable表示綁定類型,子類型和綁定類型可以是類也可以是接口。

如果這個時候,我們試圖傳入一個沒有實現接口Comparable的類的實例,將會發生編譯錯誤。

同時extends左右都允許有多個,如 T,V extends Comparable & Serializable

注意限定類型中,只允許有一個類,而且如果有類,這個類必須是限定列表的第一個。

這種類的限定既可以用在泛型方法上也可以用在泛型類上。

 

泛型中的約束和侷限性

現在我們有泛型類

不能用基本類型實例化類型參數

 運行時類型查詢只適用於原始類型 

泛型類的靜態上下文中類型變量失效

不能在靜態域或方法中引用類型變量。因爲泛型是要在對象創建的時候才知道是什麼類型的,而對象創建的代碼執行先後順序是先static的部分,然後纔是構造函數等等。所以在對象初始化之前static的部分已經執行了,如果你在靜態部分引用的泛型,那麼毫無疑問虛擬機根本不知道是什麼東西,因爲這個時候類還沒有初始化。

 

不能創建參數化類型的數組

不能實例化類型變量

不能捕獲泛型類的實例

但是這樣可以:

 

泛型類型的繼承規則

現在我們有一個類和子類

有一個泛型類

請問Pair<Employee>和Pair<Worker>是繼承關係嗎?

答案:不是,他們之間沒有什麼關係

但是泛型類可以繼承或者擴展其他泛型類,比如List和ArrayList

 

通配符類型

正是因爲前面所述的,Pair<Employee>和Pair<Worker>沒有任何關係,如果我們有一個泛型類和一個方法

現在我們有繼承關係的類

則會產生這種情況:

爲解決這個問題,於是提出了一個通配符類型 ?

有兩種使用方式:

? extends X  表示類型的上界,類型參數是X的子類

? super X  表示類型的下界,類型參數是X的超類

這兩種 方式從名字上來看,特別是super,很有迷惑性,下面我們來仔細辨析這兩種方法。

? extends X

表示傳遞給方法的參數,必須是X的子類(包括X本身)

但是對泛型類GenericType來說,如果其中提供了get和set類型參數變量的方法的話,set方法是不允許被調用的,會出現編譯錯誤

get方法則沒問題,會返回一個Fruit類型的值。

爲何?

道理很簡單,? extends X  表示類型的上界,類型參數是X的子類,那麼可以肯定的說,get方法返回的一定是個X(不管是X或者X的子類)編譯器是可以確定知道的。但是set方法只知道傳入的是個X,至於具體是X的那個子類,不知道。

總結:主要用於安全地訪問數據,可以訪問X及其子類型,並且不能寫入非null的數據。

? super X

表示傳遞給方法的參數,必須是X的超類(包括X本身)

但是對泛型類GenericType來說,如果其中提供了get和set類型參數變量的方法的話,set方法可以被調用的,且能傳入的參數只能是X或者X的子類

get方法只會返回一個Object類型的值。

爲何?

? super  X  表示類型的下界,類型參數是X的超類(包括X本身),那麼可以肯定的說,get方法返回的一定是個X的超類,那麼到底是哪個超類?不知道,但是可以肯定的說,Object一定是它的超類,所以get方法返回Object。編譯器是可以確定知道的。對於set方法來說,編譯器不知道它需要的確切類型,但是X和X的子類可以安全的轉型爲X。

總結:主要用於安全地寫入數據,可以寫入X及其子類型。

無限定的通配符 ?

表示對類型沒有什麼限制,可以把?看成所有類型的父類,如Pair< ?>;

比如:

ArrayList<T> al=new ArrayList<T>(); 指定集合元素只能是T類型

ArrayList<?> al=new ArrayList<?>();集合元素可以是任意類型,這種沒有意義,一般是方法中,只是爲了說明用法。

在使用上:

? getFirst() : 返回值只能賦給 Object,;

void setFirst(?) : setFirst 方法不能被調用, 甚至不能用 Object 調用;

虛擬機是如何實現泛型的?

泛型思想早在C++語言的模板(Template)中就開始生根發芽,在Java語言處於還沒有出現泛型的版本時,只能通過Object是所有類型的父類和類型強制轉換兩個特點的配合來實現類型泛化。,由於Java語言裏面所有的類型都繼承於java.lang.Object,所以Object轉型成任何對象都是有可能的。但是也因爲有無限的可能性,就只有程序員和運行期的虛擬機才知道這個Object到底是個什麼類型的對象。在編譯期間,編譯器無法檢查這個Object的強制轉型是否成功,如果僅僅依賴程序員去保障這項操作的正確性,許多ClassCastException的風險就會轉嫁到程序運行期之中。

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

Java語言中的泛型則不一樣,它只在程序源碼中存在,在編譯後的字節碼文件中,就已經替換爲原來的原生類型(Raw Type,也稱爲裸類型)了,並且在相應的地方插入了強制轉型代碼,因此,對於運行期的Java語言來說,ArrayList<int>與ArrayList<String>就是同一個類,所以泛型技術實際上是Java語言的一顆語法糖,Java語言中的泛型實現方法稱爲類型擦除,基於這種方法實現的泛型稱爲僞泛型。

將一段Java代碼編譯成Class文件,然後再用字節碼反編譯工具進行反編譯後,將會發現泛型都不見了,程序又變回了Java泛型出現之前的寫法,泛型類型都變回了原生類型

上面這段代碼是不能被編譯的,因爲參數List<Integer>和List<String>編譯之後都被擦除了,變成了一樣的原生類型List<E>,擦除動作導致這兩種方法的特徵簽名變得一模一樣。

由於Java泛型的引入,各種場景(虛擬機解析、反射等)下的方法調用都可能對原有的基礎產生影響和新的需求,如在泛型類中如何獲取傳入的參數化類型等。因此,JCP組織對虛擬機規範做出了相應的修改,引入了諸如Signature、LocalVariableTypeTable等新的屬性用於解決伴隨泛型而來的參數類型的識別問題,Signature是其中最重要的一項屬性,它的作用就是存儲一個方法在字節碼層面的特徵簽名[3],這個屬性中保存的參數類型並不是原生類型,而是包括了參數化類型的信息。修改後的虛擬機規範要求所有能識別49.0以上版本的Class文件的虛擬機都要能正確地識別Signature參數。

另外,從Signature屬性的出現我們還可以得出結論,擦除法所謂的擦除,僅僅是對方法的Code屬性中的字節碼進行擦除,實際上元數據中還是保留了泛型信息,這也是我們能通過反射手段取得參數化類型的根本依據。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章