天天看點

Java 5.0泛型程式設計之泛型類型

Java5.0的新特性之一是引入了泛型類型和泛型方法。一個泛型類型通過使用一個或多個類型變量來定義,并擁有一個或多個使用一個類型變量作為一個參數或者傳回值的占位符。例如,類型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>”。需要注意的是,這非常象給一個方法傳一個參數,差別是我們使用類型而不是值,同時使用尖括号而不是圓括号

  Java.util的集合類中的元素必須是對象化的,他們不能是基本類型。泛型的引入并沒有改變這點。泛型不能使用基本類型:我們不能這樣來申明——Set<char>或者List<int>。記住,無論如何,java5.0中的自動打包和自動解包特性使得使用Set<Character>或者List<Integer>和直接使用char和int值一樣友善。(檢視第二章以了解更多關于自動打包和自動解包的細節)。

  在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。這個技巧在被調用的方法上有一點不同的作用。在實際應用中,下界通配符要比上界通配符用得少。我們将在後面的章節裡讨論這個問題。