天天看點

J2SE 5.0中的泛型

轉載自:http://www.matrix.org.cn/resource/article/43/43634_java_generics.html

J2SE 5.0中的泛型

作者:Budi Kurniawan

翻譯:RR00

email:[email protected]

版權聲明:可以任意轉載,轉載時請務必以超連結形式标明文章原始出處和作者資訊及本聲明

英文原文位址:

http://www.onjava.com/pub/a/onjava/2005/07/06/generics.html

中文位址:

http://www.matrix.org.cn/resource/article/43/43634_java_generics.html

關鍵詞: java generics java5

摘要

泛型是J2SE 5.0最重要的特性。他們讓你寫一個type(類或接口)和建立一個執行個體通過傳遞一個或多個引用類型。這個執行個體受限于隻能作用于這些類型。比如,在 java 5,java.util.List 已經被泛化。當建立一個list對象時,你通過傳遞一個java類型建立一個List執行個體,此list執行個體隻能作用于所傳遞的類型。這意味着如果你傳遞一 個String ,此List執行個體隻能擁有String對象;如果你傳遞一個Integer,此執行個體隻能存貯Integer對象。除了建立參數化的類型,你還能建立參數化 的函數。

泛型的第一個好處是編譯時的嚴格類型檢查。這是集合架構最重要的特點。此外,泛型消除了絕大多數的類型轉換。在JDK 5.0之前,當你使用集合架構時,你不得不進行類型轉換。

本文将教你如何操作泛型。它的第一部分是“沒有泛型的日子”,先讓我們回憶老版本JDK的不便。然後,舉一些泛型的例子。在讨論完文法以及有界泛型的使用之後,文章最後一章将解釋如何寫泛型。

沒有泛型的日子

所有的java類都源自java.lang.Object,這意味着所有的JAVA對象能轉換成Object。是以,在之前的JDK的版本中,很 多集合架構的函數接受一個Object參數。是以,collections是一個能持有任何對象的多用途工具,但帶來了不良的後果。

舉個簡單的例子,在JDK 5.0的之前版本中,類List的函數add接受一個Object參數:

1
      
public boolean add(java.lang.Object element)
      

是以你能傳遞任何類型給add。這是故意這麼設計的。否則,它隻能傳遞某種特定的對象,這樣就會出現各種List類型,如,StringList, EmployeeList, AddressList等。

add通過Object傳遞能帶來好處,現在我們考慮get函數(傳回List中的一個元素).如下是JDK 5之前版本的定義:

1
      
public java.lang.Object get(int index) throws IndexOutOfBoundsException
      

get傳回一個Object.不幸的事情從此開始了.假如你儲存了兩個String對象在一個List中:

1
2
3
      
List stringList1 = new ArrayList();
stringList1.add("Java 5");
stringList1.add("with generics");
      

當你想從stringList1取得一個元素時,你得到了一個Object.為了操作原來的類型元素,你不得不把它轉換為String。

1
      
String s1 = (String) stringList1.get(0);
      

但是,假如你曾經把一個非String對象加入stringList1中,上面的代碼會抛出一個ClassCastException. 有了泛型,你能建立一個單一用途的List執行個體.比如,你能建立一個隻接受String對象的List執行個體,另外一個執行個體隻能接受Employee對象. 這同樣适用于集合架構中的其他類型.

泛型入門

像一個函數能接受參數一樣,一個泛型也能接受參數.這就是一個泛型經常被稱為一個參數化類型的原因.但是不像函數用()傳遞參數,泛型是用<>傳遞參數的.聲明一個泛型和聲明一個普通類沒有什麼差別,隻不過你把泛型的變量放在<>中.

比如,在JDK 5中,你可以這樣聲明一個java.util.List : List<E> myList;

E 稱為類型變量.意味着一個變量将被一個類型替代.替代類型變量的值将被當作參數或傳回類型.對于List接口來說,當一個執行個體被建立以後,E 将被當作一個add或别的函數的參數.E 也會使get或别的參數的傳回值.下面是add和get的定義:

1
2
      
boolean add<E o>
E get(int index)
      

NOTE:一個泛型在聲明或例示時允許你傳遞特定的類型變量: E.除此之外,如果E是個類,你可以傳遞子類;如果E是個接口,你可以傳遞實作接口的類;

-----------------------------譯者添加--------------------

1
2
3
      
List<Number> numberList= new ArrayList<Number>();
numberList.add(2.0);
numberList.add(2);
      

-----------------------------譯者添加--------------------

如果你傳遞一個String給一個List,比如:

1
      
List<String> myList;
      

那麼mylist的add函數将接受一個String作為他的參數,而get函數将傳回一個String.因為傳回了一個特定的類型,是以不用類型轉化了。

NOTE:根據慣例,我們使用一個唯一的大寫字目表示一個類型變量。為了建立一個泛型,你需在聲明時傳遞同樣的參數清單。比如,你要想建立一個ArrayList來操作String ,你必須把String放在<>中。如:

1
      
List<String> myList = new ArrayList<String>();
      

再比如,java.util.Map 是這麼定義的:

1
      
public interface Map<K,V>
      

K用來聲明map鍵(KEY)的類型而V用來表示值(VALUE)的類型。put和values是這麼定義的:

1
2
      
V put(K key, V value)
Collection<V> values()
      

NOTE:一個泛型不準直接的或間接的是java.lang.Throwable的子類。因為異常是在運作時抛出的,是以它不可能預言什麼類型的異常将在編譯時抛出.

清單1的例子将比較List在JDK 1.4 和JDK1.5的不同

在清單1中,stringList2是個泛型。聲明List<String>告訴編譯器List的執行個體能接受一個String對象。 當然,在另外的情況中,你能建立能接受各種對象的List執行個體。注意,當從List執行個體中傳回成員元素時,不需要對象轉化,因為他傳回的了你想要的類型, 也就是String.

NOTE:泛型的類型檢查(type checking)是在編譯時完成的.

最讓人感興趣的事情是,一個泛型是個類型并且能被當作一個類型變量。比如,你想你的List儲存lists of Strings,你能通過把List<String>作為他的類型變量來聲明List。比如:

1
      
List<List<String>> myListOfListsOfStrings;
      

要從myList中的第一個List重新取得String,你可以這麼用:

1
      
String s = myListOfListsOfStrings.get(0).get(0);
      

下一個清單中的ListOfListsTest類示範了一個List(命名為listOfLists)接受一個String List作為參數。

另外,一個泛型接受一個或多個類型變量。比如,java.util.Map有兩個類型變量s。第一個定義了鍵(key)的類型,第二個定義了值(value)的類型。下面的例子講教我們如何使用個一個泛型Map.

在這個例子中,重新得到一個key1代表的String值,我們不需要任何類型轉換。

沒有參數的情況下使用泛型

既然在J2SE 5.0中收集類型已經泛型化,那麼,原來的使用這些類型的代碼将如何呢?很幸運,他們在JAVA 5中将繼續工作,因為你能使用沒有參數的泛型。比如,你能繼續像原來一樣使用List接口,正如下面的例子一樣。

1
2
3
4
      
List stringList1 = new ArrayList();
stringList1.add("Java 1.0 - 5.0");
stringList1.add("without generics");
String s1 = (String) stringList1.get(0);
      

一個沒有任何參數的泛型被稱為原型(raw type)。它意味着這些為JDK1.4或更早的版本而寫的代碼将繼續在java 5中工作。

盡管如此,一個需要注意的事情是,JDK5編譯器希望你使用帶參數的泛型。否則,編譯器将提示警告,因為他認為你可能忘了定義類型變量s。比如,編譯上面的代碼的時候你會看到下面這些警告,因為第一個List被認為是原型。

1
2
3
      
Note: com/brainysoftware/jdk5/app16/GenericListTest.java
uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
      

當你使用原型時,如果你不想看到這些警告,你有幾個選擇來達到目的:

1.編譯時帶上參數-source 1.4

2.使用@SupressWarnings("unchecked")注釋

3.更新你的代碼,使用List<Object>. List<Object>的執行個體能接受任何類型的對象,就像是一個原型List。然而,編譯器不會報錯。

使用 ? 通配符

前面提過,如果你聲明了一個List<aType>, 那麼這個List對aType起作用,是以你能儲存下面這些類型的對象:

1.一個aType的執行個體

2.它的子類的執行個體(如果aType是個類)

3.實作aType接口的類執行個體(如果aType是個接口)

但是,請注意,一個泛型本身是個JAVA類型,就像java.lang.String或java.io.File一樣。傳遞不同的類型變量給泛型可以建立不同的JAVA類型。比如,下面例子中list1和list2引用了不同的類型對象。

1
2
      
List<Object> list1 = new ArrayList<Object>();
List<String> list2 = new ArrayList<String>();
      

list1指向了一個類型變量s為java.lang.Objects 的List而list2指向了一個類型變量s為String 的List。是以傳遞一個List<String>給一個參數為List<Object>的函數将導緻compile time錯誤。下面清單可以說明:

上面的代碼無法編譯,因為你試圖傳遞一個錯誤的類型給函數doIt。doIt的參數是List<Object>二你傳遞的參數是List<String>。

可以使用 ? 通配符解決這個難題。List<?> 意味着一個對任何對象起作用的List。是以,doIt可以改為:

在某些情況下你會考慮使用 ? 通配符。比如,你有一個printList函數,這個函數列印一個List的所有成員,你想讓這個函數對任何類型的List起作用時。否則,你隻能累死累 活的寫很多printList的重載函數。下面的清單引用了使用 ? 通配符的printList函數。

這些代碼說明了在printList函數中,List<?>表示各種類型的List對象。然而,請注意,在聲明的時候使用 ? 通配符是不合法的,像這樣:

如果你想建立一個接收任何類型對象的List,你可以使用Object作為類型變量,就像這樣:

1
      
List<Object> myList = new ArrayList<Object>();
      

在函數中使用界限通配符

在之前的章節中,你學會了通過傳遞不同的類型變量s來建立不同JAVA類型的泛型,但并不考慮類型變量s之間的繼承關系。在很多情況下,你想一個 函數有不同的List參數。比如,你有一個函數getAverage,他傳回了一個List中成員的平均值。然而,如果你把List< Number>作為getAverage的參數,你就沒法傳遞List<Integer> 或List<Double>參數,因為List<Number>和List<Integer> 和List<Double>不是同樣的類型。

你能使用原型或使用通配符,但這樣無法在編譯時進行安全類型檢查,因為你能傳遞任何類型的List,比如List<String>的 執行個體。你可以使用List<Number>作為參數,但是你就隻能傳遞List<Number>給函數。但這樣就使你的函數功能 減少,因為你可能更多的時候要操作List<Integer>或List<Long>,而不是List< Number>。

J2SE5.0增加了一個規則來解決了這種限制,這個規則就是允許你定義一個上界(upper bound) 類型變量.在這種方式中,你能傳遞一個類型或它的子類。在上面getAverage函數的例子中,你能傳遞一個List<Number>或它 的子類的執行個體,比如List<Integer> or List<Float>。

使用上界規則的文法這麼定義的:GenericType<? extends upperBoundType>. 比如,對getAverage函數的參數,你可以這麼寫List<? extends Number>. 下面例子說明了如何使用這種規則。

由于有了上界規則,上面例子中的getAverage函數允許你傳遞一個List<Number> 或一個類型變量是任何java.lang.Number子類的List。

下界規則

關鍵字extends定義了一個類型變量的上界。通過使用super關鍵字,我們可以定義一個類型變量的下界,盡管使用的情況不多。比如,如果一 個函數的參數是List<? super Integer>,那麼意味着你可以傳遞一個List<Integer>的執行個體或者任何java.lang.Integer的超類 (superclass)。

建立泛型

前面的章節主要說明了如何使使用泛型,特别是集合架構中的類。現在我們開始學習如何寫自己的泛型。

基本上,除了聲明一些你想要使用的類型變量s外,一個泛型和别的類沒有什麼差別。這些類型變量s位于類型後面的<>中。比如,下面的 Point就是個泛型。一個Point對象代表了一個系統中的點,它有橫坐标和縱坐标。通過使Point泛型化,你能定義一個點執行個體的精确程度。比如,如 果一個Point對象需要非常精确,你就把Double作為類型變量。否則,Integer 就夠了。

在這個例子中,T是Point的類型變量 。T是getX和getY的傳回值類型,也是setX和setY的參數類型。此外,構造函數結合兩個T參數。

使用point類就像使用别的類一樣。比如,下面的例子建立了兩個Point對象:ponint1和point2。前者把Integer作為類型變量,而後者把Double作為類型變量。

1
2
3
4
      
Point<Integer> point1 = new Point<Integer>(4, 2);
point1.setX(7);
Point<Double> point2 = new Point<Double>(1.3, 2.6);
point2.setX(109.91);
      

總結

泛型使代碼在編譯時有了更嚴格的類型檢查。特别是在集合架構中,泛型有兩個作用。第一,他們增加了對集合類型在編譯時的類型檢查,是以集合類所能 持有的類型對傳遞給它的參數類型起了限制作用。比如你建立了一個持有strings的java.util.List執行個體,那麼他就将不能接受 Integers或别的類型。其次,當你從一個集合中取得一個元素時,泛型消除了類型轉換的必要。

泛型能夠在沒有類型變量的情況下使用,比如,作為原型。這些措施讓Java 5之前的代碼能夠運作在JRE 5中。但是,對新的應用程式,你最好不要使用原型,因為以後Java可能不支援他們。

你已經知道通過傳遞不同類型的類型變量給泛型可以産生不同的JAVA類型。就是說List<String>和List< Object>的類型是不同的。盡管String是java.lang.Object。但是傳遞一個List<String>給一個參 數是List<Object>的函數會參數會産生編譯錯誤(compile error)。函數能用 ? 通配符使其接受任何類型的參數。List<?> 意味着任何類型的對象。

最後,你已經看到了寫一個泛型和别的一般JAVA類沒有什麼差別。你隻需要在類型名稱後面的<>中聲明一系列的類型變量s就行了。這些類型變量s就是傳回值類型或者參數類型。根據慣例,一個類型變量用一個大寫字母表示。