JAVA泛型程式設計筆記 1介紹
Java泛型程式設計是JDK1.5版本後引入的。泛型讓編程人員能夠使用類型抽象,通常用於集合裡面。下面是一個不用泛型例子:
如果Foo是Bar的子類型,G是一種帶泛型的類型,則G<Foo>不是G<Bar>的子類型。這也許是泛型學習裡面最讓人容易混淆的一點
? extends Object 重在指定接收可以是子類型 直接?表示接收任何類型(不可add)
? super String 重在add的類型是String的類或本身
注意泛型方法的格式(泛型作為方法的參數),型別參數<T>需要放在函數傳回值之前。然後在參數和傳回值中就可以使用泛型參數了
在<>中表示泛型的可以是任意字母(<T>),和問號(泛型類<?>)一樣的功能(通配),沒有具體泛型類(list,conllection)就用<T>,其中表示
其他的讀寫時的區別一樣
public abstract class PacketProcesser<P extends PaPacket,T extends Answer> Java代碼 List myIntList=new LinkedList(); //1 myIntList.add(newInteger(0)); //2 Integer x=(Integer)myIntList.iterator().next(); //3
注意第3行代碼,但這是讓人很不爽的一點,因為程式員肯定知道自己儲存在List裡面的物件類型是Integer,但是在返回列表中元素時,還是必須強制轉換類型,這是為什麼呢。原因在於,編譯器只能保證迭代器的next()方法返回的是Object類型的對象,為保證Integer變數的型別安全,所以必須強制轉換。
這種轉換不僅顯得混亂,更可能導致類型轉換異常ClassCastException,運行時異常往往讓人難以檢測到。保證列表中的元素為一個特定的資料類型,這樣就可以取消類型轉換,減少發生錯誤的機會, 這也是泛型設計的初衷。下面是一個使用了泛型的例子:
Java代碼 List<Integer> myIntList=newLinkedList<Integer>(); //1’ myIntList.add(newInteger(0)); //2’ Integerx=myIntList.iterator().next(); //3’
在第1行代碼中指定List中儲存的物件類型為Integer,這樣在擷取列表中的對象時,不必強制轉換類型了。
2定義簡單的泛型
下面是一個引用自java.util包中的介面List和Iterator的定義,其中用到了泛型技術。
Java代碼 public interface List<E> { <span style="white-space: pre;"> </span>void add(E x); <span style="white-space: pre;"> </span>Iterator<E> iterator(); } public interface Iterator<E> { <span style="white-space: pre;"> </span>E next(); <span style="white-space: pre;"> </span>boolean hasNext(); } |
這跟原生類型沒有什麼區別,只是在介面後面加入了一個角括弧,角括弧裡面是一個型別參數(定義時就是一個格式化的型別參數,在調用時會使用一個具體的類型來替換該類型)。
也許可以這樣認為,List<Integer>表示List中的型別參數E會被替換成Integer。
Java代碼 public interface IntegerList { <span style="white-space: pre;"> </span>void add(Integer x) <span style="white-space: pre;"> </span>Iterator<Integer> iterator(); }
類型擦除指的是通過型別參數合并,將泛型型別執行個體關聯到同一份位元組碼上。編譯器只為泛型型別產生一份位元組碼,並將其執行個體關聯到這份位元組碼上,因此泛型型別中的靜態變數是所有執行個體共用的。此外,需要注意的是,一個static方法,無法訪問泛型類的型別參數,因為類還沒有執行個體化,所以,若static方法需要使用泛型能力,必須使其成為泛型方法。類型擦除的關鍵在於從泛型型別中清除型別參數的相關資訊,並且再必要的時候添加類型檢查和類型轉換的方法。在使用泛型時,任何具體的類型都被擦除,唯一知道的是你在使用一個對象。比如:List<String>和List<Integer>在運行事實上是相同的類型。他們都被擦除成他們的原生類型,即List。因為編譯的時候會有類型擦除,所以不能通過同一個泛型類的執行個體來區分方法,如下面的例子編譯時間會出錯,因為類型擦除後,兩個方法都是List類型的參數,因此並不能根據泛型類的類型來區分方法。
Java代碼 /*會導致編譯時間錯誤*/ public class Erasure{ public void test(List<String> ls){ System.out.println("Sting"); } public void test(List<Integer> li){ System.out.println("Integer"); } } |
那麼這就有個問題了,既然在編譯的時候會在方法和類中擦除實際類型的資訊,那麼在返回對象時又是如何知道其具體類型的呢。如List<String>編譯後會擦除掉String資訊,那麼在運行時通過迭代器返回List中的對象時,又是如何知道List中儲存的是String類型對象呢。
擦除在方法體中移除了類型資訊,所以在運行時的問題就是邊界:即對象進入和離開方法的地點,這正是編譯器在編譯期執行類型檢查並插入轉型代碼的地點。泛型中的所有動作都發生在邊界處:對傳遞進來的值進行額外的編譯期檢查,並插入對傳遞出去的值的轉型。
3.泛型和子類型
為了徹底理解泛型,這裡看個例子:(Apple為Fruit的子類)
Java代碼 List<Apple> apples = new ArrayList<Apple>(); //1 List<Fruit> fruits = apples; //2 |
第1行代碼顯然是對的,但是第2行是否對呢。我們知道Fruit fruit = new Apple(),這樣肯定是對的,即蘋果肯定是水果,但是第2行在編譯的時候會出錯。這會讓人比較納悶的是一個蘋果是水果,為什麼一箱蘋果就不是一箱水果了呢。可以這樣考慮,我們假定第2行代碼沒有問題,那麼我們可以使用語句fruits.add(new Strawberry())(Strawberry為Fruit的子類)在fruits中加入草莓了,但是這樣的話,一個List中裝入了各種不同類型的子類水果,這顯然是不可以的,因為我們在取出List中的水果對象時,就分不清楚到底該轉型為蘋果還是草莓了。
通常來說,如果Foo是Bar的子類型,G是一種帶泛型的類型,則G<Foo>不是G<Bar>的子類型。這也許是泛型學習裡面最讓人容易混淆的一點。
4.萬用字元 4.1萬用字元。
先看一個列印集合中所有元素的代碼。
Java代碼 //不使用泛型 void printCollection(Collection c) { <span style="white-space: pre;"> </span>Iterator i=c.iterator(); <span style="white-space: pre;"> </span>for (k=0;k < c.size();k++) { <span style="white-space: pre;"> </span>System.out.println(i.next()); <span style="white-space: pre;"> </span>} }
|
Java代碼 //使用泛型 void printCollection(Collection<Object> c) { for (Object e:c) { System.out.println(e); } } |
很容易發現,使用泛型的版本只能接受元素類型為Object類型的集合如ArrayList<Object>();如果是ArrayList<String>,則會編譯時間出錯。因為我們前面說過,Collection<Object>並不是所有集合的超類。而老版本可以列印任何類型的集合,那麼如何改造新版本以便它能接受所有類型的集合呢。這個問題可以通過使用萬用字元來解決。修改後的代碼如下所示:
Java代碼 //使用萬用字元。,表示可以接收任何元素類型的集合作為參數 void printCollection(Collection<?> c) { <span style="white-space: pre;"> </span>for (Object e:c) { <span style="white-space: pre;"> </span>System.out.println(e); <span style="white-space: pre;"> </span>} } |
這裡使用了萬用字元。指定可以使用任何類型的集合作為參數。讀取的元素使用了Object類型來表示,這是安全的,因為所有的類都是Object的子類。這裡就又出現了另外一個問題,如下代碼所示,如果試圖往使用萬用字元。的集合中加入對象,就會在編譯時間出現錯誤。需要注意的是,這裡不管加入什麼類型的對象都會出錯。這是因為萬用字元。表示該集合儲存的元素類型未知,可以是任何類型。往集合中加入元素需要是一個未知元素類型的子類型,正因為該集合儲存的元素類型未知,所以我們沒法向該集合中添加任何元素。唯一的例外是null,因為null是所有類型的子類型,所以儘管元素類型不知道,但是null一定是它的子類型。