JAVA泛型编程笔记
?? 1介紹
???? Java泛型編程是JDK1.5版本后引入的。泛型讓編程人員能夠使用類型抽象,通常用于集合里面。
???? 下面是一個不用泛型例子:
????
1 List myIntList=new LinkedList(); //1 2 myIntList.add(newInteger(0)); //2 3 Integer x=(Integer)myIntList.iterator().next(); //3???
????? 注意第3行代碼,但這是讓人很不爽的一點,因為程序員肯定知道自己存儲在List里面的對象類型是Integer,但是在返回列表中元素時,還是必須強制轉換類型,這是為什么呢?原因在于,編譯器只能保證迭代器的next()方法返回的是Object類型的對象,為保證Integer變量的類型安全,所以必須強制轉換。
????? 這種轉換不僅顯得混亂,更可能導致類型轉換異常ClassCastException,運行時異常往往讓人難以檢測到。保證列表中的元素為一個特定的數據類型,這樣就可以取消類型轉換,減少發生錯誤的機會,?這也是泛型設計的初衷。下面是一個使用了泛型的例子:
1 List<Integer> myIntList=newLinkedList<Integer>(); //1’ 2 myIntList.add(newInteger(0)); //2’ 3 Integerx=myIntList.iterator().next(); //3’???在第1行代碼中指定List中存儲的對象類型為Integer,這樣在獲取列表中的對象時,不必強制轉換類型了。
?
???
2定義簡單的泛型
???? 下面是一個引用自java.util包中的接口List和Iterator的定義,其中用到了泛型技術。
1 public interface List<E> { 2 <span style="white-space: pre;"> </span>void add(E x); 3 <span style="white-space: pre;"> </span>Iterator<E> iterator(); 4 } 5 public interface Iterator<E> { 6 <span style="white-space: pre;"> </span>E next(); 7 <span style="white-space: pre;"> </span>boolean hasNext(); 8 }????? 這跟原生類型沒有什么區別,只是在接口后面加入了一個尖括號,尖括號里面是一個類型參數(定義時就是一個格式化的類型參數,在調用時會使用一個具體的類型來替換該類型)。
???????也許可以這樣認為,List<Integer>表示List中的類型參數E會被替換成Integer。
1 public interface IntegerList { 2 <span style="white-space: pre;"> </span>void add(Integer x) 3 <span style="white-space: pre;"> </span>Iterator<Integer> iterator(); 4 }????? 類型擦除指的是通過類型參數合并,將泛型類型實例關聯到同一份字節碼上。編譯器只為泛型類型生成一份字節碼,并將其實例關聯到這份字節碼上,因此泛型類型中的靜態變量是所有實例共享的。此外,需要注意的是,一個static方法,無法訪問泛型類的類型參數,因為類還沒有實例化,所以,若static方法需要使用泛型能力,必須使其成為泛型方法。類型擦除的關鍵在于從泛型類型中清除類型參數的相關信息,并且再必要的時候添加類型檢查和類型轉換的方法。在使用泛型時,任何具體的類型都被擦除,唯一知道的是你在使用一個對象。比如:List<String>和List<Integer>在運行事實上是相同的類型。他們都被擦除成他們的原生類型,即List。因為編譯的時候會有類型擦除,所以不能通過同一個泛型類的實例來區分方法,如下面的例子編譯時會出錯,因為類型擦除后,兩個方法都是List類型的參數,因此并不能根據泛型類的類型來區分方法。
1 /*會導致編譯時錯誤*/ 2 public class Erasure{ 3 public void test(List<String> ls){ 4 System.out.println("Sting"); 5 } 6 public void test(List<Integer> li){ 7 System.out.println("Integer"); 8 } 9 }??????那么這就有個問題了,既然在編譯的時候會在方法和類中擦除實際類型的信息,那么在返回對象時又是如何知道其具體類型的呢?如List<String>編譯后會擦除掉String信息,那么在運行時通過迭代器返回List中的對象時,又是如何知道List中存儲的是String類型對象呢?
???????擦除在方法體中移除了類型信息,所以在運行時的問題就是邊界:即對象進入和離開方法的地點,這正是編譯器在編譯期執行類型檢查并插入轉型代碼的地點。泛型中的所有動作都發生在邊界處:對傳遞進來的值進行額外的編譯期檢查,并插入對傳遞出去的值的轉型。
?
???
??? 3.泛型和子類型
????? 為了徹底理解泛型,這里看個例子:(Apple為Fruit的子類)
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通配符?
???? 先看一個打印集合中所有元素的代碼。
1 //不使用泛型 2 void printCollection(Collection c) { 3 <span style="white-space: pre;"> </span>Iterator i=c.iterator(); 4 <span style="white-space: pre;"> </span>for (k=0;k < c.size();k++) { 5 <span style="white-space: pre;"> </span>System.out.println(i.next()); 6 <span style="white-space: pre;"> </span>} 7 } 1 //使用泛型 2 void printCollection(Collection<Object> c) { 3 for (Object e:c) { 4 System.out.println(e); 5 } 6 }????? 很容易發現,使用泛型的版本只能接受元素類型為Object類型的集合如ArrayList<Object>();如果是ArrayList<String>,則會編譯時出錯。因為我們前面說過,Collection<Object>并不是所有集合的超類。而老版本可以打印任何類型的集合,那么如何改造新版本以便它能接受所有類型的集合呢?這個問題可以通過使用通配符來解決。修改后的代碼如下所示:
//使用通配符?,表示可以接收任何元素類型的集合作為參數 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一定是它的子類型。
?
???? 另一方面,我們可以從List<?> lists中獲取對象,雖然不知道List中存儲的是什么類型,但是可以肯定的是存儲的類型一定是Object的子類型,所以可以用Object類型來獲取值。如for(Object obj: lists),這是合法的。
?? 4.2邊界通配符
?? 1)?extends通配符
假定有一個畫圖的應用,可以畫各種形狀的圖形,如矩形和圓形等。為了在程序里面表示,定義如下的類層次:
???? 為了畫出集合中所有的形狀,我們可以定義一個函數,該函數接受帶有泛型的集合類對象作為參數。但是不幸的是,我們只能接收元素類型為Shape的List對象,而不能接收類型為List<Cycle>的對象,這在前面已經說過。為了解決這個問題,所以有了邊界通配符的概念。這里可以采用public void drawAll(List<? extends Shape>shapes)來滿足條件,這樣就可以接收元素類型為Shape子類型的列表作為參數了。
?
?
?????? 這里就又有個問題要注意了,如果我們希望在List<?exends Shape> shapes中加入一個矩形對象,如下所示:
shapes.add(0, new Rectangle()); //compile-time error
那么這時會出現一個編譯時錯誤,原因在于:我們只知道shapes中的元素時Shape類型的子類型,具體是什么子類型我們并不清楚,所以我們不能往shapes中加入任何類型的對象。不過我們在取出其中對象時,可以使用Shape類型來取值,因為雖然我們不知道列表中的元素類型具體是什么類型,但是我們肯定的是它一定是Shape類的子類型。
?
?????? 2)?super通配符
???????這里還有一種邊界通配符為?super。比如下面的代碼:
這表示cicleSupers列表存儲的元素為Cicle的超類,因此我們可以往其中加入Cicle對象或者Cicle的子類對象,但是不能加入Shape對象。這里的原因在于列表cicleSupers存儲的元素類型為Cicle的超類,但是具體是Cicle的什么超類并不清楚。但是我們可以確定的是只要是Cicle或者Circle的子類,則一定是與該元素類別兼容。
?
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)泛型數組問題
不能創建一個確切泛型類型的數組。如下面代碼會出錯。
List<String>[] lsa = new ArrayList<String>[10];?//compile error.
???????因為如果可以這樣,那么考慮如下代碼,會導致運行時錯誤。
因此只能創建帶通配符的泛型數組,如下面例子所示,這回可以通過編譯,但是在倒數第二行代碼中必須顯式的轉型才行,即便如此,最后還是會拋出類型轉換異常,因為存儲在lsa中的是List<Integer>類型的對象,而不是List<String>類型。最后一行代碼是正確的,類型匹配,不會拋出異常。
?
轉載于:https://www.cnblogs.com/UniqueColor/p/5712358.html
總結
以上是生活随笔為你收集整理的JAVA泛型编程笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: frame和bounds
- 下一篇: 行内框可以修改的尺寸