天天看點

泛型總結

泛型總結

 1介紹

  Java泛型程式設計是JDK1.5版本後引入的。泛型讓程式設計人員能夠使用類型抽象,通常用于集合裡面。下面是一個不用泛型例子:

  注意第三行的代碼,讓人很不爽,因為程式員肯定知道自己存儲在List裡面的對象類型是Integer,但是在傳回的時候,清單中元素必須強制轉換,這是為什麼呢?原因在于,編譯器隻能保證疊代器的next()方法傳回的是Object類型的對象,為保證Interger變量的類型安全,必須強制轉換。

這種轉換不僅顯得混亂,而且導緻轉換異常ClassCastException,運作時異常往往讓人難以檢測到。保證清單中的元素為一個特定的資料類型,這樣就可以取消類型轉換,減少發生錯誤的機會,這也是泛型設計的初衷。下面給出一個泛型的例子:

  在第一行代碼中指定List中存儲的對象類型是Integer,這樣在擷取清單中的對象時,不必強制類型轉換了。

 2 定義簡單的泛型

  下面是一個引用java.util包中的借口List和Iterator的定義,其中用到了泛型技術。

  這跟原生态類型沒有什麼差別,隻是在接口後面加入了一個尖括号,尖括号裡面是一個類型參數(定義時就是一個格式化的類型參數,在調用時會使用一個具體的類型來替換該類型)。

  也許可以這樣認為,List<Integer>表示List中的類型參數E會被替換成Integer。

  類型擦除指的是通過類型參數的合并,将泛型類型執行個體關聯到同一個位元組碼上,編譯器隻為泛型類型生成一個位元組碼,并将其關聯到這上面,是以泛型類型中的靜态變量是所有執行個體共享的。此外需要注意,一個static方法,無法通路泛型類的類型參數,因為類還沒有執行個體化,是以若static方法需要使用泛型,必須使其成為泛型方法。

  類型擦除的關鍵在于從泛型類中清除類型參數的相關資訊,并且再必要的時候添加類型檢查和類型轉換的方法。使用泛型時,任何具體的類型都被擦除,唯一知道的是你在使用一個對象。比如List<String>和List<Integer>是相同的類型。它們都被擦除成原始類型,即List。

  因為編譯的時候會有類型擦除,是以不能通過一個泛型類的執行個體來區分方法,如下面的例子編譯會出錯,因為類型擦除後,兩個方法都是List類型的參數。是以不能根據泛型類的類型來區分方法。

  那麼有問題了,既然編譯時會在方法和類中擦除實際類型的資訊,那麼傳回對象時又是如何知道具體類型的呢?如List<String>編譯後會擦除String資訊,那麼在運作時通過疊代器傳回List中的對象時,又是如何知道List中存儲的是String類型的對象的呢?

擦除在方法中的類型資訊,是以在運作時的問題是邊界:即對象進入和離開方法的地點,這正是編譯器在編譯期執行類型檢查并出入轉換代碼的地點。泛型中的所有動作都發生在邊界處:對傳遞進來的值進行額外的編譯器檢查,并插入對傳遞出去的值的轉換。

3 泛型和子類型

  為了徹底了解泛型,看個例子,(Apple為Fruit的子類)

  這裡第一行顯然是對的,但是第2行是否對呢?我們知道Fruit fruit = new Apple(),這樣肯定是對的,即蘋果肯定是水果,但是第2行在編譯的時候會出錯。這會讓人比較納悶的是一個蘋果是水果,為什麼一箱蘋果就不是一箱水果了呢?可以這樣考慮,我們假定第2行代碼沒有問題,那麼我們可以使用語句fruits.add(new Strawberry())(Strawberry為Fruit的子類)在fruits中加入草莓了,但是這樣的話,一個List中裝入了各種不同類型的子類水果,這顯然是不可以的,因為我們在取出List中的水果對象時,就分不清楚到底該轉型為蘋果還是草莓了。(因為擦除後,蘋果和草莓都變成了Fruit類型,具體傳回對象的時候,不能區分哪個是蘋果的對象,哪個是草莓的對象。)

  通常來說,如果Foo是Bar的子類型,G是一種帶泛型的類型,則G<Foo>不是G<Bar>的子類型。這是容易混淆的地方。

4 通配符

4.1 通配符?

  先看一個列印集合所有元素的代碼。

  很容易發現,使用泛型的版本隻接受類型為Object類型的集合,如ArrayList<Object>();如果是ArrayList<String>,則會出錯。因為前面說過,Collection<Object>并不是所有集合的超類。而老版本可以列印任意類型的集合,那麼改造新版本以便能接受所有類型的集合呢?這個問題可以通過通配符解決。修改後的代碼如下:

   這裡使用了通配符?指定可以使用任何類型的集合作為參數。讀取元素使用了Objectect類型表示,這是安全的,因為所有的類都是Object的子類。

 又有另一個問題,如下面代碼所示,如果試圖往使用通配符?的集合中加入對象,會出錯。需要注意,不管加入什麼類型的對象都會出錯。這是因為統配符表示該集合存儲的元素類型未知,可以是任意的。

  另一方面,我們可以從List<?>lists中擷取值,如for(Object obj:lists),這是合法的,因為可以肯定存儲類型一定是Object的子類型,是以可以用Object類型來擷取。

4.2 邊界通配符

1)?extends通配符

  假定有一個畫圖的應用,可以活各種形狀。為了在程式裡面表示,定義如下的類層次。

  有一個問題,如果我們希望List<? extends Shapes> shapes中加入一個矩形對象,如下所示:

shapes.add(0, new Rectangle());//編譯出錯。

  原因是:我們隻知道shapes中的元素時Shapes類型的子類型。具體是什麼子類不知道。是以不能加入任何類型的對象。不過我們在取出對象時,可以用Shape類型來取值,因為雖然不知道清單中元素是什麼類型,但是它一定是Shape類的子類型。

2)?super通配符

  這裡還有一種邊界通配符?super。如:

  這裡cicleSupers清單中元素是Cicle的超類,是以,我們可以往其中加入Cicle對象或者是Cicle子類的對象,但是不能加入Shape對象。這裡的原因在于清單cicleSupers存儲的是Cicle的超類,但具體類型未知。

3)邊界通配符總結

<!--[if !supportLists]-->l        <!--[endif]-->如果你想從一個資料類型裡擷取資料,使用 ? extends 通配符

<!--[if !supportLists]-->l        <!--[endif]-->如果你想把對象寫入一個資料結構裡,使用 ? super 通配符

<!--[if !supportLists]-->l        <!--[endif]-->如果你既想存,又想取,那就别用通配符。

5 泛型方法

  考慮實作一個方法,該方法拷貝一個數組中的所有對象到集合中。下面是初始的版本。

  可以看到顯然會出現錯誤,原因在于之前講過,因為集合c中的類型未知,是以不能往其中加入任何的對象(當然,null除外)。解決該問題的好方法是使用泛型方法。

  泛型方法的格式,類型參數<T>要放在函數傳回值之前,然後參數和傳回值中就可以使用泛型參數了,具體一些調用方法的執行個體如下:

  注意到我們調用該方法時并不需要傳遞類型參數,系統會自動判斷參數并調用合适的方法。當然在某些情況下需要制定傳遞類型參數,比如當存在與泛型方法相同的方法的時候(方法參數不一樣),如下面的例子:

  當不指定類型參數時,調用的是普通的方法,如果指定了類型參數,則調用泛型方法。可這樣了解,因為泛型方法編譯後類型擦除,如果不指定類型參數,則泛型方法此時相當于是public void go(Object t)。而普通的方法接收參數為String類型,是以String類型的實參調用函數,肯定會調用形參為String的普通方法了。如果是以Object類型的實參調用函數,肯定會調用泛型方法。

6 需要注意的地方

1)方法重載

  在JAVA裡面方法重載是不能通過傳回值類型來區分的,比如代碼一中一個類中定義兩個如下的方法是不容許的。但是當參數為泛型類型時,确實可以的,如代碼二,雖然形參經過類型擦除後都以List類型,但是傳回類型不同,這是可以的。

2)泛型類型是被所有調用共享的

  所有泛型類型的執行個體都共享同一個運作時類,類型參數資訊會在編譯時被擦除。是以考慮如下代碼,雖然ArrayList<String>和ArrayList<Integer>類型參數不同,但是它們都共享ArrayList類,是以結果會是true。

3)instanceof

  不能對确切的泛型類型使用instanceof()操作,下面代碼是違法的。

4)泛型數組問題

  不能建立一個确切檢討類型的數組,否則出錯。

  是以隻能建立帶通配符的泛型數組,如下面例子所示,這回可以通過編譯,但倒數第二行代碼必須顯示的轉型才行,即便如此,最後還是會抛出類型轉換異常,因為存儲在lsa中的List<Integer>類型的對象,而不是List<String>類型。最後一行代碼是正确的,類型比對,不會抛出異常。

當神已無能為力,那便是魔渡衆生