一個泛型型別透過使用一個或多個型別變數來定義,並擁有一個或多個使用一個型別變數作為一個引數或者返回值的佔位符。例如,型別java.util.List<E>是一個泛型型別:一個list,其元素的型別被佔位符E描述。這個型別有一個名為add()的方法,被宣告為有一個型別為E的引數,同時,有一個get()方法,返回值被宣告為E型別。
為了使用泛型型別,你應該為型別變數詳細指明實際的型別,形成一個就像List<String>類似的引數化型別。[1]指明這些額外的型別資訊的原因是編譯器據此能夠在編譯期為您提供很強的型別檢查,增強您的程式的型別安全性。舉個例子來說,您有一個只能保持String物件的List,那麼這種型別檢查就能夠阻止您往裡面加入String[]物件。同樣的,增加的型別資訊使編譯器能夠為您做一些型別轉換的事情。比如,編譯器知道了一個List<String>有個get()方法,其返回值是一個String物件,因此您不再需要去將返回值由一個Object強制轉換為String。
Java.util包中的集合類在java5.0中已經被做成了泛型,也許您將會在您的程式中頻繁的使用到他們。型別安全的集合類就是一個泛型型別的典型案例。即便您從沒有定義過您自己的泛型型別甚至從未用過除了java.util中的集合類以外的泛型型別,型別安全的集合類的好處也是極有意義的一個標誌——他們證明了這個主要的新語言特性的複雜性。
我們從探索型別安全的集合類中的基本的泛型用法開始,進而研究更多使用泛型型別的複雜細節。然後我們討論型別引數萬用字元和有界萬用字元。描繪瞭如何使用泛型以後,我們闡明如何編寫自己的泛型型別和泛型方法。我們對於泛型的討論將結束於一趟對於JavaAPI的核心中重要的泛型型別的旅行。這趟旅程將探索這些型別以及他們的用法,旅程的目的是為了讓您對泛型如何工作這個問題有個深入的理解。
型別安全集合類
Java.util類包包含了Java集合框架(Java Collections Framework),這是一批包含物件的set、物件的list以及基於key-value的map。第五章將談到集合類。這裡,我們討論的是在java5.0中集合類使用型別引數來界定集合中的物件的型別。這個討論並不適合java1.4或更早期版本。如果沒有泛型,對於集合類的使用需要程式設計師記住每個集合中元素的型別。當您在java1.4種建立了一個集合,您知道您放入到集合中的物件的型別,但是編譯器不知道。您必須小心地往其中加入一個合適型別的元素,當需要從集合中獲取元素時,您必須顯式的寫強制型別轉換以將他們從Object轉換為他們真是的型別。考察下邊的java1.4的程式碼。
public static void main(String[] args) {
// This list is intended to hold only strings.
// The compiler doesn"t know that so we have to remember ourselves.
List wordlist = new ArrayList();
// Oops! We added a String[] instead of a String.
// The compiler doesn"t know that this is an error.
wordlist.add(args);
// Since List can hold arbitrary objects, the get() method returns
// Object. Since the list is intended to hold strings, we cast the
// return value to String but get a ClassCastException because of
// the error above.
String word = (String)wordlist.get(0);
}
泛型型別解決了這段程式碼中的顯示的型別安全問題。Java.util中的List或是其他集合類已經使用泛型重寫過了。就像前面提到的, List被重新定義為一個list,它中間的元素型別被一個型別可變的名稱為E的佔位符描述。Add()方法被重新定義為期望一個型別為E的引數,用於替換以前的Object,get()方法被重新定義為返回一個E,替換了以前的Object。
在java5.0中,當我們申明一個List或者建立一個ArrayList的例項的時候,我們需要在泛型型別的名字後面緊跟一對“<>”,尖括號中寫入我們需要的實際的型別。比如,一個保持String的List應該寫成“List<String>”。需要注意的是,這非常象給一個方法傳一個引數,區別是我們使用型別而不是值,同時使用尖括號而不是圓括號
在Java5.0中,上面的例子將被重寫為如下方式:
// This list can only hold String objects
List<String> wordlist = new ArrayList<String>();
// args is a String[], not String, so the compiler won"t let us do this
wordlist.add(args); // Compilation error!
// We can do this, though.
// Notice the use of the new for/in looping statement
for(String arg : args) wordlist.add(arg);
// No cast is required. List<String>.get() returns a String.
String word = wordlist.get(0);
值得注意的是程式碼量其實並沒有比原來那個沒有泛型的例子少多少。使用“(String)”這樣的型別轉換被替換成了型別引數“<String>”。 不同的是型別引數需要且僅需要宣告一次,而list能夠被使用任何多次,不需要型別轉換。在更長點的例子程式碼中,這一點將更加明顯。即使在那些看上去泛型語法比非泛型語法要冗長的例子裡,使用泛型依然是非常有價值的——額外的型別資訊允許編譯器在您的程式碼裡執行更強的錯誤檢查。以前只能在執行起才能發現的錯誤現在能夠在編譯時就被發現。此外,以前為了處理型別轉換的異常,我們需要新增額外的程式碼行。如果沒有泛型,那麼當發生型別轉換異常的時候,一個ClassCastException異常就會被從實際程式碼中丟擲。
就像一個方法可以使用任意數量的引數一樣,類允許使用多個型別變數。介面Java.util.Map就是一個例子。一個Map體現了從一個key的物件到一個value的物件的對映關係。介面Map申明瞭一個型別變數來描述key的型別而另一個型別變數來描述value的型別。舉個例子來說,假設您希望做一個String物件到Integer物件的對映關係:
// A map from strings to their position in the args[] array
Map<String,Integer> map = new HashMap<String,Integer>();
// Note that we use autoboxing to wrap i in an Integer object.
for(int i=0; i < args.length; i++) map.put(args[i], i);
// Find the array index of a word. Note no cast is required!
Integer position = map.get("hello");
// We can also rely on autounboxing to convert directly to an int,
// but this throws a NullPointerException if the key does not exist
// in the map
int pos = map.get("world");
象List<String>這個一個引數型別其本身也是也一個型別,也能夠被用於當作其他型別的一個型別變數值。您可能會看到這樣的程式碼:
// Look at all those nested angle brackets!
Map<String, List<List<int[]>>> map = getWeirdMap();
// The compiler knows all the types and we can write expressions
// like this without casting. We might still get NullPointerException
// or ArrayIndexOutOfBounds at runtime, of course.
int value = map.get(key).get(0).get(0)[0];
// Here"s how we break that expression down step by step.
List<List<int[]>> listOfLists = map.get(key);
List<int[]> listOfIntArrays = listOfLists.get(0);
int[] array = listOfIntArrays.get(0);
int element = array[0];
在上面的程式碼裡,java.util.List<E>和java.util.Map<K,V>的get()方法返回一個型別為E的list元素或者一個型別為V的map元素。注意,無論如何,泛型型別能夠更精密的使用他們的變數。在本書中的參考章節檢視List<E>,您將會看到它的iterator( )方法被宣告為返回一個Iterator<E>。這意味著,這個方法返回一個跟list的實際的引數型別一樣的一個引數型別的例項。為了具體的說明這點,下面的例子提供了不使用get(0)方法來獲取一個List<String>的第一個元素的方法。
List<String> words = // ...initialized elsewhere...
Iterator<String> iterator = words.iterator();
String firstword = iterator.next();
理解泛型型別
本段將對泛型型別的使用細節做進一步的探討,以嘗試說明下列問題:
不帶型別引數的使用泛型的後果
引數化型別的體系
一個關於編譯期泛型型別的型別安全的漏洞和一個用於確保執行期型別安全的補丁
為什麼引數化型別的陣列不是型別安全的
未經處理的型別和不被檢查的警告
即使被重寫的Java集合類帶來了泛型的好處,在使用他們的時候您也不被要求說明型別變數。一個不帶型別變數的泛型型別被認為是一個未經處理的型別(raw type)。這樣,5.0版本以前的java程式碼仍然能夠執行:您顯式的編寫所有型別轉換就像您已經這樣寫的一樣,您可能會被一些來自編譯器的麻煩所困擾。檢視下列儲存不同型別的物件到一個未經處理的List:
List l = new ArrayList();
l.add("hello");
l.add(new Integer(123));
Object o = l.get(0);
這段程式碼在java1.4下執行得很好。如果您用java5.0來編譯它,javac編譯了,但是會打印出這樣的“抱怨”:
Note: Test.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
如果我們加入-Xlint引數後重新編譯,我們會看到這些警告:
Test.java:6: warning: [unchecked]
unchecked call to add(E) as a member of the raw type java.util.List
^
Test.java:7: warning: [unchecked]
編譯在add()方法的呼叫上給出了警告,因為它不能夠確信加入到list中的值具有正確的型別。它告訴我們說我們使用了一個未經處理的型別,它不能驗證我們的程式碼是型別安全的。注意,get()方法的呼叫是沒有問題的,因為能夠被獲得的元素已經安全的存在於list中了。
如果您不想使用任何的java5.0的新特性,您可以簡單的透過帶-source1.4標記來編譯他們,這樣編譯器就不會再“抱怨”了。如果您不能這樣做,您可以忽略這些警告,透過使用一個“@SuppressWarnings("unchecked")”註解(檢視本章的4.3節)隱瞞這些警告資訊或者升級您的程式碼,加入型別變數描述。[2]下列示例程式碼,編譯的時候不再會有警告但仍然允許您往list中放入不同的型別的物件。
List<Object> l = new ArrayList<Object>();
l.add(123); // autoboxing
引數化型別有型別體系,就像一般的型別一樣。這個體系基於物件的型別,而不是變數的型別。這裡有些例子您可以嘗試:
ArrayList<Integer> l = new ArrayList<Integer>();
List<Integer> m = l; // okay
Collection<Integer> n = l; // okay
ArrayList<Number> o = l; // error
Collection<Object> p = (Collection<Object>)l; // error, even with cast
一個List<Integer>是一個Collection<Integer>,但不是一個List<Object>。這句話不容易理解,如果您想理解為什麼泛型這樣做,這段值得看一下。考察這段程式碼:
List<Integer> li = new ArrayList<Integer>();
li.add(123);
// The line below will not compile. But for the purposes of this
// thought-experiment, assume that it does compile and see how much
// trouble we get ourselves into.
List<Object> lo = li;
// Now we can retrieve elements of the list as Object instead of Integer
Object number = lo.get(0);
// But what about this?
lo.add("hello world");
// If the line above is allowed then the line below throws ClassCastException
Integer i = li.get(1); // Can"t cast a String to Integer!
這就是為什麼List<Integer>不是一個List<Object>的原因,雖然List<Integer>中所有的元素事實上是一個Object的例項。如果允許轉換成List<Object>,那麼轉換後,理論上非整型的物件也將被允許新增到list中。
執行時型別安全
就像我們所見到的,一個List<X>不允許被轉換為一個List<Y>,即使這個X能夠被轉換為Y。然而,一個List<X>能夠被轉換為一個List,這樣您就可以透過繼承的方法來做這樣的事情。
這種將引數化型別轉換為非引數化型別的能力對於向下相容是必要的,但是它會在泛型所帶來的型別安全體系上鑿個漏洞:
// Here"s a basic parameterized list.
// It is legal to assign a parameterized type to a nonparameterized variable
List l = li;
// This line is a bug, but it compiles and runs.
// The Java 5.0 compiler will issue an unchecked warning about it.
// If it appeared as part of a legacy class compiled with Java 1.4, however,
// then we"d never even get the warning.
// This line compiles without warning but throws ClassCastException at runtime.
// Note that the failure can occur far away from the actual bug.
Integer i = li.get(0);
泛型僅提供了編譯期的型別安全。如果您使用java5.0的編譯器來編譯您的程式碼並且沒有得到任何警告,這些編譯器的檢查能夠確保您的程式碼在執行期也是型別安全的。如果您獲得了警告或者使用了像未經處理的型別那樣修改您的集合的程式碼,那麼您需要增加一些步驟來確保執行期的型別安全。您可以透過使用java.util.Collections中的checkedList()和checkedMap( )方法來做到這一步。這些方法將把您的集合打包成一個wrapper集合,從而在執行時檢查確認只有正確型別的值能夠被置入集合眾。下面是一個能夠補上型別安全漏洞的一個例子:
// Wrap it for runtime type safety
List<Integer> cli = Collections.checkedList(li, Integer.class);
// Now widen the checked list to the raw type
List l = cli;
// This line compiles but fails at runtime with a ClassCastException.
// The exception occurs exactly where the bug is, rather than far away
引數化型別的陣列
在使用泛型型別的時候,陣列需要特別的考慮。回憶一下,如果T是S的父類(或者介面),那麼型別為S的陣列S[],同時又是型別為T的陣列T[]。正因為如此,每次您存放一個物件到陣列中時,Java直譯器都必須進行檢查以確保您放入的物件型別與要存放的陣列所允許的型別是匹對的。例如,下列程式碼在執行期會檢查失敗,丟擲一個ArrayStoreException異常:
String[] words = new String[10];
Object[] objs = words;
objs[0] = 1; // 1 autoboxed to an Integer, throws ArrayStoreException
雖然編譯時obj是一個Object[],但是在執行時它是一個String[],它不允許被用於存放一個Integer.
當我們使用泛型型別的時候,僅僅依靠執行時的陣列存放異常檢查是不夠的,因為一個執行時進行的檢查並不能夠獲取編譯時的型別引數資訊。檢視下列程式碼:
List<String>[] wordlists = new ArrayList<String>[10];
ArrayList<Integer> ali = new ArrayList<Integer>();
ali.add(123);
Object[] objs = wordlists;
objs[0] = ali; // No ArrayStoreException
String s = wordlists[0].get(0); // ClassCastException!
如果上面的程式碼被允許,那麼執行時的陣列儲存檢查將會成功:沒有編譯時的型別引數,程式碼簡單地儲存一個ArrayList到一個ArrayList[]陣列,非常正確。既然編譯器不能阻止您透過這個方法來戰勝型別安全,那麼它轉而阻止您建立一個引數化型別的陣列。所以上述情節永遠不會發生,編譯器在第一行就開始拒絕編譯了。
型別引數萬用字元
假設我們需要寫一個方法來顯示一個List中的元素。[3]在以前,我們只需要象這樣寫段程式碼:
public static void printList(PrintWriter out, List list) {
for(int i=0, n=list.size(); i < n; i++) {
if (i > 0) out.print(", ");
out.print(list.get(i).toString());
在Java5.0中,List是一個泛型型別,如果我們試圖編譯這個方法,我們將會得到unchecked警告。為了解決這些警告,您可能需要這樣來修改這個方法:
public static void printList(PrintWriter out, List<Object> list) {
這段程式碼能夠編譯透過同時不會有警告,但是它並不是非常地有效,因為只有那些被宣告為List<Object>的list才會被允許使用這個方法。還記得麼,類似於List<String>和List<Integer>這樣的List並不能被轉型為List<Object>。事實上我們需要一個型別安全的printList()方法,它能夠接受我們傳入的任何List,而不關心它被引數化為什麼。解決辦法是使用型別引數萬用字元。方法可以被修改成這樣:
public static void printList(PrintWriter out, List<?> list) {
Object o = list.get(i);
out.print(o.toString());
這個版本的方法能夠被編譯過,沒有警告,而且能夠在任何我們希望使用的地方使用。萬用字元“?”表示一個未知型別,型別List<?>被讀作“List of unknown”
作為一般原則,如果型別是泛型的,同時您並不知道或者並不關心值的型別,您應該使用“?”萬用字元來代替一個未經處理的型別。未經處理的型別被允許僅是為了向下相容,而且應該只能夠被允許出現在老的程式碼中。注意,無論如何,您不能在呼叫構造器時使用萬用字元。下面的程式碼是非法的:
List<?> l = new ArrayList<?>();
建立一個不知道型別的List是毫無道理的。如果您建立了它,那麼您必須知道它將保持的元素是什麼型別的。您可以在隨後的方法中不關心元素型別而去遍歷這裡list,但是您需要在您建立它的時候描述元素的型別。如果你確實需要一個List來保持任何型別,那麼您只能這麼寫:
從上面的printList()例子中,必須要搞清楚List<?>既不是List<Object>也不是一個未經處理的List。一個使用萬用字元的List<?>有兩個重要的特性。第一,考察類似於get()的方法,他們被宣告返回一個值,這個值的型別是型別引數中指定的。在這個例子中,型別是“unknown”,所以這些方法返回一個Object。既然我們期望的是呼叫這個object的toString()方法,程式能夠很好的滿足我們的意願。
第二,考察List的類似add()的方法,他們被宣告為接受一個引數,這個引數被型別引數所定義。出人意料的是,當型別引數是未確定的,編譯器不允許您呼叫任何有不確定引數型別的方法——因為它不能確認您傳入了一個恰當的值。一個List(?)實際上是隻讀的——既然編譯器不允許我們呼叫類似於add(),set(),addAll()這類的方法。
界定萬用字元
讓我們在我們原來的例子上作些小小的稍微複雜一點的改動。假設我們希望寫一個sumList()方法來計算list中Number型別的值的合計。在以前,我們使用未經處理的List,但是我們不想放棄型別安全,同時不得不處理來自編譯器的unchecked警告。或者我們可以使用List<Number>,那樣的話我們就不能呼叫List<Integer>、List<Double>中的方法了,而事實上我們需要呼叫。如果我們使用萬用字元,那麼我們實際上不能得到我們期望的型別安全,我們不能確定我們的方法被什麼樣的List所呼叫,Number?還是Number的子類?甚至,String?這樣的一個方法也許會被寫成這樣:
public static double sumList(List<?> list) {
double total = 0.0;
for(Object o : list) {
Number n = (Number) o; // A cast is required and may fail
total += n.doubleValue();
return total;
要修改這個方法讓它變得真正的型別安全,我們需要使用界定萬用字元(bounded wildcard),能夠確保List的型別引數是未知的,但又是Number或者Number的子類。下面的程式碼才是我們想要的:
public static double sumList(List<? extends Number> list) {
for(Number n : list) total += n.doubleValue();
型別List<? extends Number>可以被理解為“Number未知子類的List”。理解這點非常重要,在這段文字中,Number被認為是其自身的子類。
注意,這樣的話,那些型別轉換已經不再需要了。我們並不知道list中元素的具體型別,但是我們知道他們能夠向上轉型為Number,因此我們可以把他們從list中把他們當作一個Number物件取出。使用一個for/in迴圈能夠稍微封裝一下從list中取出元素的過程。普遍性的原則是當您使用一個界定萬用字元時,類似於List中的get()方法的那些方法將返回一個型別為上界的值。因此如果我們在for/in迴圈中呼叫list.get(),我們將得到一個Number。在前一節說到使用萬用字元時類似於list.add()這種方法中的限制依然有效:舉個例子來說,如果編譯器允許我們呼叫這類方法,我們就可以將一個Integer放到一個宣告為僅保持Short值的list中去。
同樣可行的是使用下界萬用字元,不同的是用super替換extends。這個技巧在被呼叫的方法上有一點不同的作用。在實際應用中,下界萬用字元要比上界萬用字元用得少。
一個泛型型別透過使用一個或多個型別變數來定義,並擁有一個或多個使用一個型別變數作為一個引數或者返回值的佔位符。例如,型別java.util.List<E>是一個泛型型別:一個list,其元素的型別被佔位符E描述。這個型別有一個名為add()的方法,被宣告為有一個型別為E的引數,同時,有一個get()方法,返回值被宣告為E型別。
為了使用泛型型別,你應該為型別變數詳細指明實際的型別,形成一個就像List<String>類似的引數化型別。[1]指明這些額外的型別資訊的原因是編譯器據此能夠在編譯期為您提供很強的型別檢查,增強您的程式的型別安全性。舉個例子來說,您有一個只能保持String物件的List,那麼這種型別檢查就能夠阻止您往裡面加入String[]物件。同樣的,增加的型別資訊使編譯器能夠為您做一些型別轉換的事情。比如,編譯器知道了一個List<String>有個get()方法,其返回值是一個String物件,因此您不再需要去將返回值由一個Object強制轉換為String。
Java.util包中的集合類在java5.0中已經被做成了泛型,也許您將會在您的程式中頻繁的使用到他們。型別安全的集合類就是一個泛型型別的典型案例。即便您從沒有定義過您自己的泛型型別甚至從未用過除了java.util中的集合類以外的泛型型別,型別安全的集合類的好處也是極有意義的一個標誌——他們證明了這個主要的新語言特性的複雜性。
我們從探索型別安全的集合類中的基本的泛型用法開始,進而研究更多使用泛型型別的複雜細節。然後我們討論型別引數萬用字元和有界萬用字元。描繪瞭如何使用泛型以後,我們闡明如何編寫自己的泛型型別和泛型方法。我們對於泛型的討論將結束於一趟對於JavaAPI的核心中重要的泛型型別的旅行。這趟旅程將探索這些型別以及他們的用法,旅程的目的是為了讓您對泛型如何工作這個問題有個深入的理解。
型別安全集合類
Java.util類包包含了Java集合框架(Java Collections Framework),這是一批包含物件的set、物件的list以及基於key-value的map。第五章將談到集合類。這裡,我們討論的是在java5.0中集合類使用型別引數來界定集合中的物件的型別。這個討論並不適合java1.4或更早期版本。如果沒有泛型,對於集合類的使用需要程式設計師記住每個集合中元素的型別。當您在java1.4種建立了一個集合,您知道您放入到集合中的物件的型別,但是編譯器不知道。您必須小心地往其中加入一個合適型別的元素,當需要從集合中獲取元素時,您必須顯式的寫強制型別轉換以將他們從Object轉換為他們真是的型別。考察下邊的java1.4的程式碼。
public static void main(String[] args) {
// This list is intended to hold only strings.
// The compiler doesn"t know that so we have to remember ourselves.
List wordlist = new ArrayList();
// Oops! We added a String[] instead of a String.
// The compiler doesn"t know that this is an error.
wordlist.add(args);
// Since List can hold arbitrary objects, the get() method returns
// Object. Since the list is intended to hold strings, we cast the
// return value to String but get a ClassCastException because of
// the error above.
String word = (String)wordlist.get(0);
}
泛型型別解決了這段程式碼中的顯示的型別安全問題。Java.util中的List或是其他集合類已經使用泛型重寫過了。就像前面提到的, List被重新定義為一個list,它中間的元素型別被一個型別可變的名稱為E的佔位符描述。Add()方法被重新定義為期望一個型別為E的引數,用於替換以前的Object,get()方法被重新定義為返回一個E,替換了以前的Object。
在java5.0中,當我們申明一個List或者建立一個ArrayList的例項的時候,我們需要在泛型型別的名字後面緊跟一對“<>”,尖括號中寫入我們需要的實際的型別。比如,一個保持String的List應該寫成“List<String>”。需要注意的是,這非常象給一個方法傳一個引數,區別是我們使用型別而不是值,同時使用尖括號而不是圓括號
在Java5.0中,上面的例子將被重寫為如下方式:
public static void main(String[] args) {
// This list can only hold String objects
List<String> wordlist = new ArrayList<String>();
// args is a String[], not String, so the compiler won"t let us do this
wordlist.add(args); // Compilation error!
// We can do this, though.
// Notice the use of the new for/in looping statement
for(String arg : args) wordlist.add(arg);
// No cast is required. List<String>.get() returns a String.
String word = wordlist.get(0);
}
值得注意的是程式碼量其實並沒有比原來那個沒有泛型的例子少多少。使用“(String)”這樣的型別轉換被替換成了型別引數“<String>”。 不同的是型別引數需要且僅需要宣告一次,而list能夠被使用任何多次,不需要型別轉換。在更長點的例子程式碼中,這一點將更加明顯。即使在那些看上去泛型語法比非泛型語法要冗長的例子裡,使用泛型依然是非常有價值的——額外的型別資訊允許編譯器在您的程式碼裡執行更強的錯誤檢查。以前只能在執行起才能發現的錯誤現在能夠在編譯時就被發現。此外,以前為了處理型別轉換的異常,我們需要新增額外的程式碼行。如果沒有泛型,那麼當發生型別轉換異常的時候,一個ClassCastException異常就會被從實際程式碼中丟擲。
就像一個方法可以使用任意數量的引數一樣,類允許使用多個型別變數。介面Java.util.Map就是一個例子。一個Map體現了從一個key的物件到一個value的物件的對映關係。介面Map申明瞭一個型別變數來描述key的型別而另一個型別變數來描述value的型別。舉個例子來說,假設您希望做一個String物件到Integer物件的對映關係:
public static void main(String[] args) {
// A map from strings to their position in the args[] array
Map<String,Integer> map = new HashMap<String,Integer>();
// Note that we use autoboxing to wrap i in an Integer object.
for(int i=0; i < args.length; i++) map.put(args[i], i);
// Find the array index of a word. Note no cast is required!
Integer position = map.get("hello");
// We can also rely on autounboxing to convert directly to an int,
// but this throws a NullPointerException if the key does not exist
// in the map
int pos = map.get("world");
}
象List<String>這個一個引數型別其本身也是也一個型別,也能夠被用於當作其他型別的一個型別變數值。您可能會看到這樣的程式碼:
// Look at all those nested angle brackets!
Map<String, List<List<int[]>>> map = getWeirdMap();
// The compiler knows all the types and we can write expressions
// like this without casting. We might still get NullPointerException
// or ArrayIndexOutOfBounds at runtime, of course.
int value = map.get(key).get(0).get(0)[0];
// Here"s how we break that expression down step by step.
List<List<int[]>> listOfLists = map.get(key);
List<int[]> listOfIntArrays = listOfLists.get(0);
int[] array = listOfIntArrays.get(0);
int element = array[0];
在上面的程式碼裡,java.util.List<E>和java.util.Map<K,V>的get()方法返回一個型別為E的list元素或者一個型別為V的map元素。注意,無論如何,泛型型別能夠更精密的使用他們的變數。在本書中的參考章節檢視List<E>,您將會看到它的iterator( )方法被宣告為返回一個Iterator<E>。這意味著,這個方法返回一個跟list的實際的引數型別一樣的一個引數型別的例項。為了具體的說明這點,下面的例子提供了不使用get(0)方法來獲取一個List<String>的第一個元素的方法。
List<String> words = // ...initialized elsewhere...
Iterator<String> iterator = words.iterator();
String firstword = iterator.next();
理解泛型型別
本段將對泛型型別的使用細節做進一步的探討,以嘗試說明下列問題:
不帶型別引數的使用泛型的後果
引數化型別的體系
一個關於編譯期泛型型別的型別安全的漏洞和一個用於確保執行期型別安全的補丁
為什麼引數化型別的陣列不是型別安全的
未經處理的型別和不被檢查的警告
即使被重寫的Java集合類帶來了泛型的好處,在使用他們的時候您也不被要求說明型別變數。一個不帶型別變數的泛型型別被認為是一個未經處理的型別(raw type)。這樣,5.0版本以前的java程式碼仍然能夠執行:您顯式的編寫所有型別轉換就像您已經這樣寫的一樣,您可能會被一些來自編譯器的麻煩所困擾。檢視下列儲存不同型別的物件到一個未經處理的List:
List l = new ArrayList();
l.add("hello");
l.add(new Integer(123));
Object o = l.get(0);
這段程式碼在java1.4下執行得很好。如果您用java5.0來編譯它,javac編譯了,但是會打印出這樣的“抱怨”:
Note: Test.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
如果我們加入-Xlint引數後重新編譯,我們會看到這些警告:
Test.java:6: warning: [unchecked]
unchecked call to add(E) as a member of the raw type java.util.List
l.add("hello");
^
Test.java:7: warning: [unchecked]
unchecked call to add(E) as a member of the raw type java.util.List
l.add(new Integer(123));
^
編譯在add()方法的呼叫上給出了警告,因為它不能夠確信加入到list中的值具有正確的型別。它告訴我們說我們使用了一個未經處理的型別,它不能驗證我們的程式碼是型別安全的。注意,get()方法的呼叫是沒有問題的,因為能夠被獲得的元素已經安全的存在於list中了。
如果您不想使用任何的java5.0的新特性,您可以簡單的透過帶-source1.4標記來編譯他們,這樣編譯器就不會再“抱怨”了。如果您不能這樣做,您可以忽略這些警告,透過使用一個“@SuppressWarnings("unchecked")”註解(檢視本章的4.3節)隱瞞這些警告資訊或者升級您的程式碼,加入型別變數描述。[2]下列示例程式碼,編譯的時候不再會有警告但仍然允許您往list中放入不同的型別的物件。
List<Object> l = new ArrayList<Object>();
l.add("hello");
l.add(123); // autoboxing
Object o = l.get(0);
引數化型別的體系
引數化型別有型別體系,就像一般的型別一樣。這個體系基於物件的型別,而不是變數的型別。這裡有些例子您可以嘗試:
ArrayList<Integer> l = new ArrayList<Integer>();
List<Integer> m = l; // okay
Collection<Integer> n = l; // okay
ArrayList<Number> o = l; // error
Collection<Object> p = (Collection<Object>)l; // error, even with cast
一個List<Integer>是一個Collection<Integer>,但不是一個List<Object>。這句話不容易理解,如果您想理解為什麼泛型這樣做,這段值得看一下。考察這段程式碼:
List<Integer> li = new ArrayList<Integer>();
li.add(123);
// The line below will not compile. But for the purposes of this
// thought-experiment, assume that it does compile and see how much
// trouble we get ourselves into.
List<Object> lo = li;
// Now we can retrieve elements of the list as Object instead of Integer
Object number = lo.get(0);
// But what about this?
lo.add("hello world");
// If the line above is allowed then the line below throws ClassCastException
Integer i = li.get(1); // Can"t cast a String to Integer!
這就是為什麼List<Integer>不是一個List<Object>的原因,雖然List<Integer>中所有的元素事實上是一個Object的例項。如果允許轉換成List<Object>,那麼轉換後,理論上非整型的物件也將被允許新增到list中。
執行時型別安全
就像我們所見到的,一個List<X>不允許被轉換為一個List<Y>,即使這個X能夠被轉換為Y。然而,一個List<X>能夠被轉換為一個List,這樣您就可以透過繼承的方法來做這樣的事情。
這種將引數化型別轉換為非引數化型別的能力對於向下相容是必要的,但是它會在泛型所帶來的型別安全體系上鑿個漏洞:
// Here"s a basic parameterized list.
List<Integer> li = new ArrayList<Integer>();
// It is legal to assign a parameterized type to a nonparameterized variable
List l = li;
// This line is a bug, but it compiles and runs.
// The Java 5.0 compiler will issue an unchecked warning about it.
// If it appeared as part of a legacy class compiled with Java 1.4, however,
// then we"d never even get the warning.
l.add("hello");
// This line compiles without warning but throws ClassCastException at runtime.
// Note that the failure can occur far away from the actual bug.
Integer i = li.get(0);
泛型僅提供了編譯期的型別安全。如果您使用java5.0的編譯器來編譯您的程式碼並且沒有得到任何警告,這些編譯器的檢查能夠確保您的程式碼在執行期也是型別安全的。如果您獲得了警告或者使用了像未經處理的型別那樣修改您的集合的程式碼,那麼您需要增加一些步驟來確保執行期的型別安全。您可以透過使用java.util.Collections中的checkedList()和checkedMap( )方法來做到這一步。這些方法將把您的集合打包成一個wrapper集合,從而在執行時檢查確認只有正確型別的值能夠被置入集合眾。下面是一個能夠補上型別安全漏洞的一個例子:
// Here"s a basic parameterized list.
List<Integer> li = new ArrayList<Integer>();
// Wrap it for runtime type safety
List<Integer> cli = Collections.checkedList(li, Integer.class);
// Now widen the checked list to the raw type
List l = cli;
// This line compiles but fails at runtime with a ClassCastException.
// The exception occurs exactly where the bug is, rather than far away
l.add("hello");
引數化型別的陣列
在使用泛型型別的時候,陣列需要特別的考慮。回憶一下,如果T是S的父類(或者介面),那麼型別為S的陣列S[],同時又是型別為T的陣列T[]。正因為如此,每次您存放一個物件到陣列中時,Java直譯器都必須進行檢查以確保您放入的物件型別與要存放的陣列所允許的型別是匹對的。例如,下列程式碼在執行期會檢查失敗,丟擲一個ArrayStoreException異常:
String[] words = new String[10];
Object[] objs = words;
objs[0] = 1; // 1 autoboxed to an Integer, throws ArrayStoreException
雖然編譯時obj是一個Object[],但是在執行時它是一個String[],它不允許被用於存放一個Integer.
當我們使用泛型型別的時候,僅僅依靠執行時的陣列存放異常檢查是不夠的,因為一個執行時進行的檢查並不能夠獲取編譯時的型別引數資訊。檢視下列程式碼:
List<String>[] wordlists = new ArrayList<String>[10];
ArrayList<Integer> ali = new ArrayList<Integer>();
ali.add(123);
Object[] objs = wordlists;
objs[0] = ali; // No ArrayStoreException
String s = wordlists[0].get(0); // ClassCastException!
如果上面的程式碼被允許,那麼執行時的陣列儲存檢查將會成功:沒有編譯時的型別引數,程式碼簡單地儲存一個ArrayList到一個ArrayList[]陣列,非常正確。既然編譯器不能阻止您透過這個方法來戰勝型別安全,那麼它轉而阻止您建立一個引數化型別的陣列。所以上述情節永遠不會發生,編譯器在第一行就開始拒絕編譯了。
型別引數萬用字元
假設我們需要寫一個方法來顯示一個List中的元素。[3]在以前,我們只需要象這樣寫段程式碼:
public static void printList(PrintWriter out, List list) {
for(int i=0, n=list.size(); i < n; i++) {
if (i > 0) out.print(", ");
out.print(list.get(i).toString());
}
}
在Java5.0中,List是一個泛型型別,如果我們試圖編譯這個方法,我們將會得到unchecked警告。為了解決這些警告,您可能需要這樣來修改這個方法:
public static void printList(PrintWriter out, List<Object> list) {
for(int i=0, n=list.size(); i < n; i++) {
if (i > 0) out.print(", ");
out.print(list.get(i).toString());
}
}
這段程式碼能夠編譯透過同時不會有警告,但是它並不是非常地有效,因為只有那些被宣告為List<Object>的list才會被允許使用這個方法。還記得麼,類似於List<String>和List<Integer>這樣的List並不能被轉型為List<Object>。事實上我們需要一個型別安全的printList()方法,它能夠接受我們傳入的任何List,而不關心它被引數化為什麼。解決辦法是使用型別引數萬用字元。方法可以被修改成這樣:
public static void printList(PrintWriter out, List<?> list) {
for(int i=0, n=list.size(); i < n; i++) {
if (i > 0) out.print(", ");
Object o = list.get(i);
out.print(o.toString());
}
}
這個版本的方法能夠被編譯過,沒有警告,而且能夠在任何我們希望使用的地方使用。萬用字元“?”表示一個未知型別,型別List<?>被讀作“List of unknown”
作為一般原則,如果型別是泛型的,同時您並不知道或者並不關心值的型別,您應該使用“?”萬用字元來代替一個未經處理的型別。未經處理的型別被允許僅是為了向下相容,而且應該只能夠被允許出現在老的程式碼中。注意,無論如何,您不能在呼叫構造器時使用萬用字元。下面的程式碼是非法的:
List<?> l = new ArrayList<?>();
建立一個不知道型別的List是毫無道理的。如果您建立了它,那麼您必須知道它將保持的元素是什麼型別的。您可以在隨後的方法中不關心元素型別而去遍歷這裡list,但是您需要在您建立它的時候描述元素的型別。如果你確實需要一個List來保持任何型別,那麼您只能這麼寫:
List<Object> l = new ArrayList<Object>();
從上面的printList()例子中,必須要搞清楚List<?>既不是List<Object>也不是一個未經處理的List。一個使用萬用字元的List<?>有兩個重要的特性。第一,考察類似於get()的方法,他們被宣告返回一個值,這個值的型別是型別引數中指定的。在這個例子中,型別是“unknown”,所以這些方法返回一個Object。既然我們期望的是呼叫這個object的toString()方法,程式能夠很好的滿足我們的意願。
第二,考察List的類似add()的方法,他們被宣告為接受一個引數,這個引數被型別引數所定義。出人意料的是,當型別引數是未確定的,編譯器不允許您呼叫任何有不確定引數型別的方法——因為它不能確認您傳入了一個恰當的值。一個List(?)實際上是隻讀的——既然編譯器不允許我們呼叫類似於add(),set(),addAll()這類的方法。
界定萬用字元
讓我們在我們原來的例子上作些小小的稍微複雜一點的改動。假設我們希望寫一個sumList()方法來計算list中Number型別的值的合計。在以前,我們使用未經處理的List,但是我們不想放棄型別安全,同時不得不處理來自編譯器的unchecked警告。或者我們可以使用List<Number>,那樣的話我們就不能呼叫List<Integer>、List<Double>中的方法了,而事實上我們需要呼叫。如果我們使用萬用字元,那麼我們實際上不能得到我們期望的型別安全,我們不能確定我們的方法被什麼樣的List所呼叫,Number?還是Number的子類?甚至,String?這樣的一個方法也許會被寫成這樣:
public static double sumList(List<?> list) {
double total = 0.0;
for(Object o : list) {
Number n = (Number) o; // A cast is required and may fail
total += n.doubleValue();
}
return total;
}
要修改這個方法讓它變得真正的型別安全,我們需要使用界定萬用字元(bounded wildcard),能夠確保List的型別引數是未知的,但又是Number或者Number的子類。下面的程式碼才是我們想要的:
public static double sumList(List<? extends Number> list) {
double total = 0.0;
for(Number n : list) total += n.doubleValue();
return total;
}
型別List<? extends Number>可以被理解為“Number未知子類的List”。理解這點非常重要,在這段文字中,Number被認為是其自身的子類。
注意,這樣的話,那些型別轉換已經不再需要了。我們並不知道list中元素的具體型別,但是我們知道他們能夠向上轉型為Number,因此我們可以把他們從list中把他們當作一個Number物件取出。使用一個for/in迴圈能夠稍微封裝一下從list中取出元素的過程。普遍性的原則是當您使用一個界定萬用字元時,類似於List中的get()方法的那些方法將返回一個型別為上界的值。因此如果我們在for/in迴圈中呼叫list.get(),我們將得到一個Number。在前一節說到使用萬用字元時類似於list.add()這種方法中的限制依然有效:舉個例子來說,如果編譯器允許我們呼叫這類方法,我們就可以將一個Integer放到一個宣告為僅保持Short值的list中去。
同樣可行的是使用下界萬用字元,不同的是用super替換extends。這個技巧在被呼叫的方法上有一點不同的作用。在實際應用中,下界萬用字元要比上界萬用字元用得少。