Java泛型(Generics)

參考:http://docs.oracle.com/javase/tutorial/java/generics/index.html

爲什麼要使用泛型?

  • 更強更嚴格的編譯期間類型檢查
  • 淘汰類型造型
沒有泛型的話,下面的代碼需要使用造型,否則類型檢查會失敗

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
如果有泛型,則不需要造型

List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);   // no cast

  • 實現使用泛型的算法,相同的代碼可以運用於不同類型的集合

1. 泛型類型
泛型類型是一個使用類型作爲參數的類或者接口,可以像下面這樣定義一個泛型類,<>緊隨類名之後,裏面放一些類型參數也叫類型變量,類體中的代碼可以使用類型參數作爲它的類型,當使用這個類時,用實際的類型替換類型參數,類中使用這個類型參數的變量就會變成該類型。
class name<T1, T2, ..., Tn> { /* ... */ }
1.1 泛型類實例:
public class Box<T> {
    // T stands for "Type"
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}
類型變量(實例中的T)可以在類體內當做類型來使用,類型變量可以是主類型以外的任何類型:任何的類、任何接口、任何類型的數組、甚至其他類型變量。

1.2 類型參數命名慣例
按照慣例,類型參數的名字是單個大寫字母,常用類型參數名如下:
  • E - Element (廣泛使用在Java的集合框架)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types
1.3 調用及實例化泛型
使用上例中的泛型類 Box,需要用一個實際的類型替換T,相當於調用了一個參數爲T的泛型轉換函數:
Box<Integer> integerBox;
這個語句並沒有真的產生一個對象,更普通類一樣,要使用new關鍵字創建對象,注意new之後的類型也要用<Integer>指定具體類型:
Box<Integer> integerBox = new Box<Integer>();
但是在Java SE 7之後,new之後的類型參數可以忽略,但是<>還是要保留:
Box<Integer> integerBox = new Box<>();

1.4 多個類型參數
在一個泛型中可以使用多個類型參數,如下例
public interface Pair<K, V> {
    public K getKey();
    public V getValue();
}

public class OrderedPair<K, V> implements Pair<K, V> {

    private K key;
    private V value;

    public OrderedPair(K key, V value) {
	this.key = key;
	this.value = value;
    }

    public K getKey()	{ return key; }
    public V getValue() { return value; }
}
使用這個泛型類時要提供多個對應的類型替換其中的類型參數:
Pair<String, Integer> p1 = new OrderddPair<String, Integer>("Even", 8); //Java SE 7之後,可以省略new後面的類型參數 new OrderedPair<>("Even", 8);
Pair<String, String> p2 = new OrderedPair<String, String>("hello", "world");

也可以用一個參數化類型(比如List<String>)替換泛型中的類型參數:
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));


2. 原類型
2.1 原類型是指不用任何實際類型代替類型參數的類型,比如Box的例子
public class Box<T> {
    public void set(T t) { /* ... */ }
    // ...
}
創建一個參數化類型需要將T替換成一個實際的類型比如Integer
Box<Integer> intBox = new Box<>();
但是原類型就不需要替換T,也不需要<>:
Box rawBox = new Box();
所以Box就是泛型類Box<T>的原類型,但是那些並非泛型的普通類就不是原類型。

爲什麼有原類型?很多的庫包括集合庫裏面含有大量的沒有實現泛型的遺留代碼,引入原類型是爲了向後兼容,將一個參數化類型賦給一個原類型參數是允許的:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox; //沒問題
但是如果將一個原類型賦給參數化類型就會有warning:
Box rawBox = new Box();
Box<Integer> intBox = rawBox; //Warning: unchecked conversion
使用原類型調用泛型方法也會有warning:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox; //可以的
rawBox.set(8); //waring: unchecked invocation to set(T)

記住原類型只是爲了向後兼容,應儘量避免使用。

2.2 Unchecked Error Messages
我們在使用比較老的API操作原類型時,可能會遇到下列錯誤:
Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint : unchecked for details

比如下例:
public class WarningDemo {
    public static void main(String[] args){
        Box<Integer> bi;
        bi = createBox();  //unchecked error
    }

    static Box createBox(){
        return new Box();
    }
}

3. 泛型方法
擁有自己的類型參數的方法就是泛型方法,跟定義泛型類相似,但是泛型方法的類型參數被限制在方法的定義範圍內,意思是隻能在方法內部使用。
定義泛型方法的語法爲在方法返回類型前加一個<>,類型參數放置在裏面。
public class Util {
    // Generic static method
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}

public class Pair<K, V> {

    private K key;
    private V value;

    // Generic constructor
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    // Generic methods
    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    public K getKey()   { return key; }
    public V getValue() { return value; }
}
使用方法如下:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2); //這裏明確給出了泛型方法的參數類型,但是其實可以省略,編譯器會自動查找並添加正確的類型參數,可以用這個代替:boolean same = Util.compare(p1, p2); 省略參數類型的特性叫做類型推斷(type inference),它可以讓我們像普通方法一樣調用泛型方法。


4. 參數類型限制
在使用泛型時,有時想要限制使用的類型種類,比如一個操作數字的泛型方法只接受Number對象或者其子類對象,這種需求可以通過參數類型限制實現。
實現參數限制,先列出參數類型的名字,後面緊跟extends關鍵字,然後指定一個類型上限,下例中是Number。注意,這裏的extends既指類中的extends又指接口中的implements。
這個例子中,泛型方法inspect將類型限定爲Number,如果在代碼中使用Number之外的類型作爲參數,編譯器就會報錯。
public class Box<T> {

    private T t;          

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public <U <strong>extends Number</strong>> void inspect(U u){
        System.out.println("T: " + t.getClass().getName());
        System.out.println("U: " + u.getClass().getName());
    }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        integerBox.set(new Integer(10));
        integerBox.inspect("some text"); // <strong>error: this is still String! 編譯器報錯</strong>
    }
}

參數限制允許你調用上限類型(就是一個類)中定義的方法,如下例,參數類型被限制爲Integer(或者任何Integer的子類),那我們就可以直接調用Integer裏的方法intValue():
public class NaturalNumber<T extends Integer> {

    private T n;

    public NaturalNumber(T n)  { this.n = n; }

    public boolean isEven() {
        return n.intValue() % 2 == 0;
    }

    // ...
}

多重限制
我們也可以將類型參數限制在多個類型上,比如<T extends B1 & B2 & B3>。
如果參數類型限制中有一個類,那必須將這個類放在第一個位置,否則無法編譯通過:
Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }

class D <T extends A & B & C> { /* ... */ } //如果換成class D<T extends B & A & C> { /* ... */ },則編譯器會報錯

泛型方法中的參數類型限制:
參數類型限制是實現泛型算法的關鍵,下例中的方法計算出數組T[]中大於elem的元素個數。如果不使用參數限制將無法編譯,因爲大於運算符'>'只能用於主類型中
public static <T> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e > elem)  // compiler error
            ++count;
    return count;
}
比較對象不能使用‘>',我們應該使用對象的compareTo()方法,這樣的話我們需要將泛型類型限制爲泛型接口Comparable<T>,修改代碼如下
public static <T <strong>extends Comparable<T></strong>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.<strong>compareTo</strong>(elem) > 0)
            ++count;
    return count;
}


5. 泛型、繼承以及子類型
在Java中,如果類是兼容的(一個類繼承自另一個類),那麼我們可以把其中一個類對象賦值給另一個類句柄,比如Integer對象可以賦值給Object句柄,因爲Integer本身也是一個Object,這個在面向對象中叫“is a”關係:Integer “is a” Object。在泛型中也類似:
Box<Number> box = new Box<Number>();
box.add(new Integer(10));   // OK
box.add(new Double(10.1));  // OK

現在有如下的一個泛型方法,思考下它可以接受哪些類型?
public void boxTest(Box<Number> n) { /* ... */ }
從下圖中可以看出,這個方法只接受類型爲Box<Number>的參數,其他的如Box<Integer>其實並非Box<Number>的子類,不滿足“is a”原則。


對於兩個實際的類型A、B(比如Number和Integer),儘管A和B可能是父子關係(“is a”關係),但是泛型類MyClass<A>和MyClass<B>半毛錢關係都沒有,他們的共同祖先是最頂級的Object。


5.1 泛型類及子類型
泛型類或接口也可以繼承和實現,他們的類型參數之間的關係由extends和implements關鍵字決定。以Collections相關類爲例,ArrayList<E>實現了List<E>接口,List<E>實現了Collection<E>,所以ArrayList<String>是List<String>的子類,List<String>呢又是Collection<String>的子類,只要不改變類型參數,這些類之間的父子關係就不會變。


現在我們自己定義一個List接口,PayloadList,這個接口會爲每個list元素配置一個泛型爲P的可選值,聲明如下:
interface PayloadList<E,P> extends List<E> {
  void setPayload(int index, P val);
  ...
}
那麼下面這些類型都是List<String>的子類
PayloadList<String,String>
PayloadList<String,Integer>
PayloadList<String,Exception>



6. 類型推斷
類型推斷是Java編譯器的一種功能,通過檢查方法調用和聲明來推斷出合適的類型參數,推斷算法會推斷方法參數的類型已經返回值類型(如果存在的話),通過分析代碼中的信息最終找到一個適合所有參數的最合適的類型。如下例中,類型推斷方法pick的類型應爲Serializable:
static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());

6.1 泛型方法中的類型推斷
public class BoxDemo {

  public static <U> void addBox(U u, 
      java.util.List<Box<U>> boxes) {
    Box<U> box = new Box<>();
    box.set(u);
    boxes.add(box);
  }

  public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
    int counter = 0;
    for (Box<U> box: boxes) {
      U boxContents = box.get();
      System.out.println("Box #" + counter + " contains [" +
             boxContents.toString() + "]");
      counter++;
    }
  }

  public static void main(String[] args) {
    java.util.ArrayList<Box<Integer>> listOfIntegerBoxes =
      new java.util.ArrayList<>();
    BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);  //正常的調用方法,不用推斷
    BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);  //去掉<Integer>,但是編譯器可以推斷出addBox()方法的類型爲<Integer>
    BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes); //同上
    BoxDemo.outputBoxes(listOfIntegerBoxes); //同樣,listOfIntegerBoxes中已經知道類型爲<Integer>,編譯器推斷出outputBoxes()方法類型也是<Integer>
  }
}

6.2 泛型類實例化中的類型推斷
在new一個泛型對象是,我們可以用空的<>來調用構建器,編譯器可以根據上下文推斷出要new的對象的參數類型:
Map<String, List<String>> myMap = new HashMap<String, List<String>>();
//可以用下面的語句代替
Map<String, List<String>> myMap = new HashMap<>();
但是創建泛型類對象時<>不能省,否則會包unchecked conversion警告:
Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning


6.3 泛型類非泛型類的泛型構建器中的類型推斷
類(泛型或者非泛型)的構建器也可以使泛型的,下例中類和它的構造器使用了不同的類型參數,這是允許的
class MyClass<X> {
  <T> MyClass(T t) {
    // ...
  }
}
new一個對象是可以這樣:
new MyClass<Integer>("")
注意這個new語句指明瞭類中的類型參數爲Integer,構建器中的參數類型沒有明確指定,編譯器推斷出類型爲String。

6.4 目標類型
目標類型是編譯器根據表達式所在位置推斷出的數據類型,比如Collections.emptyList()方法:
static <T> List<T> emptyList();
List<String> listOne = Collections.emptyList(); //這個語句的目的是獲取List<String>實例,String就是目標類型,編譯器據此推斷出List<T>的類型是List<String>
但是在SE7的有些情形中,編譯器不能推斷出類型,比如下面的函數使用了List<String>類型參數
void processStringList(List<String> stringList) {
    // process stringList
}
我們想通過如下語句調用這個方法,但是這樣的話SE 7會得到編譯錯誤:List<Object> cannot be converted to List<String>,SE 8可以編譯通過:
processStringList(Collections.emptyList());
SE 7中需要明確指定emptyList()返回的參數類型:
processStringList(Collections.<String>emptyList());


7. 通配符
泛型中,問號(?)叫做通配符,表示一個未知的類型。通配符可以用於很多情形:參數、域、本地變量的類型,也可用於返回類型。但是不能作爲泛型方法調用、泛型類對象創建以及超類型的類型參數使用。

7.1 上邊界通配符
通過使用上邊界通配符,我們可以讓變量(泛型)適用更多類型。比如要寫一個方法,它的參數可以是List<Integer>、List<Double>、List<Number>,這種情況就可以通過上邊界通配符實現。語法如下,通配符(?)跟extends關鍵字,後面緊跟上邊界類。
<? extends [upper bound]>
比如一個方法以類型爲Number及其子類的List爲參數,這個參數可以寫成List<? extends Number>,這樣List,Integer>, List<Double>, List<Number>等都是合法的參數。如果寫成List<Number>,則只有List<Number>才合法。

7.2 無邊界通配符
沒有邊界的通配符語法像這樣:List<?>,叫做類型未知List。無邊界通配符有兩種用處:
  • 想寫一個可以用Object類裏的功能實現的方法
  • 代碼裏用到不使用類型參數的泛型類中的方法,比如List.size()或者List.clear(),Class<?>被經常用到,因爲Class<T>裏的大多數方法都不使用T
下面這個方法printList的目的是打印出任何類型的List,但是這樣寫不能達到目的,這個方法只能打印出一個Object對象列表List<Object>,不能打印比如List<Integer>, List<Number>等,因爲它們不是List<Object>的子類(它們的父類是Object)
public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}
想達到打印任何類型List的目的,就要使用無邊界通配符List<?>,因爲對任何的實際類型A,List<A>都是List<?>的子類型
public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}


7.3 下邊界通配符
與上邊界通配符類似,可以通過關鍵字super來定義下邊界通配符:<? super [lower bound]>。上邊界和下邊界不能同時設置!
實例:寫一個使用類型爲Integer的List爲參數的方法,如果想提高靈活性,允許這個方法接受List<Integer>, List<Number>, List<Object>等,類型爲Integer或者其父類型的List,這種情況可以通過下邊界通配符實現
public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

7.4 通配符和子類型化
前文提到過,類型參數之間有關係(父子)的泛型類或接口之間並沒有相應的父子關係。但是通過子類型化,我們可以在泛型類或接口之間建立父子關係
List<B> lb = new ArrayList<>();
List<A> la = lb;   // compile-time error,因爲List<A>和List<B>之間是沒有關係的,他們共同的父類是List<?>
可通過定義上邊界的方法創建有父子關係的類
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; //List<? extends Integer>是List<? extends Number>的子類
完全的關係圖如下:


7.5 通配符匹配(Wildcard Capture)和幫助者方法
編譯器有時可以通過代碼上下文推斷出通配符的確定類型,叫做通配符匹配。
一般情況下我們不必管通配符匹配,除非遇到包含“capture of”的錯誤,下例在編譯時會產生這樣的error:
import java.util.List;
public class WildcardError {
    void foo(List<?> i) {
        i.set(0, i.get(0));
    }
}
原因是從編譯器的角度看,方法foo的參數List<?> i是一個類型爲Object的List。編譯器認爲你傳了一個錯誤的類型給參數,泛型就是用來防止這種情況的,使用泛型可以進行跟強的類型檢查。

那麼我們怎麼解決這種錯誤呢?我們可以通過一個私有的幫助者方法(helper method)來識別通配符,這個解決方法是通過類型推斷識別出正確的類型:
public class WildcardFixed {
    void foo(List<?> i) {
        fooHelper(i);
    }

    // Helper method created so that the wildcard can be captured
    // through type inference.
    <strong>private <T> void fooHelper(List<T> l) {
        l.set(0, l.get(0));
    }</strong>
}

現在看一個更復雜的例子:
import java.util.List;

public class WildcardErrorBad {

    void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
      Number temp = l1.get(0);
      l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
                            // got a CAP#2 extends Number;
                            // same bound, but different types
      l2.set(0, temp);	    // expected a CAP#1 extends Number,
                            // got a Number
    }
}
這個例子中,代碼裏有一些不安全的操作。比如像下面這樣調用swapFirst方法,雖然List<Integer>和List<Double>都滿足List<? extends Number>的條件,但是將Integer對象插入一個Double類型的List顯示是不合法的。
List<Integer> li = Arrays.asList(1, 2, 3);
List<Double>  ld = Arrays.asList(10.10, 20.20, 30.30);
swapFirst(li, ld);
這種情況是不能通過helper方法來解決的,因爲這個代碼本身就是錯誤的。


7.6 通配符使用原則
在泛型中,什麼時候使用上邊界,什麼時候使用下邊界有時非常confuse,我們可以通過以下原則來決定:
  • “in”變量一般用上邊界通配符,使用extends關鍵字
  • “out”變量用下邊界通配符,使用super關鍵字
  • 如果“in”變量可以被類中的方法訪問,則定義爲無邊界通配符
  • 既是“in”又是“out”的變量,不使用通配符
定義一下“in”和“out”變量:
“in”變量:爲代碼提供數據的變量,比如copy(src, dest)中的src變量
“out”變量:獲取數據以便在其他地方使用,比如copy(src,dest)中的dest變量


8. 類型清除
Java引入泛型以進行更嚴格的編譯期類型檢查,爲實現泛型,Java編譯器在如下情況下會使用類型清除。類型清除保證運行時不會爲參數化類型生成新的類,這樣泛型機制就不會帶來多餘的運行時開銷:
  • 如果類型參數是無邊界的,編譯器會用Object取代所有泛型類型參數,如果是有邊界的,則用邊界類取代所有泛型類型參數,這種情況下,字節碼就只包含普通的類、接口以及方法了
  • 必要時插入類型造型,保護類型安全
  • 自動生成橋方法,以保留擴展泛型類型的多態性
8.1 泛型類型的清除
如果類型參數是無邊界的,編譯器會用Object取代所有泛型類型參數,如果是有邊界的,則用邊界類取代所有泛型類型參數。
下例中泛型是無邊界的,所以編譯時這些泛型類型都會被替換成Object
public class Node<T> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) }
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}
無邊界的泛型類型T被替換成Object,如下
public class Node {

    private Object data;
    private Node next;

    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Object getData() { return data; }
    // ...
}

如果泛型類型有邊界,則替換成邊界類/接口
public class Node<T extends Comparable<T>> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}
泛型類型T被替換成Comparable
public class Node {

    private Comparable data;
    private Node next;

    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Comparable getData() { return data; }
    // ...
}

8.2 泛型方法的類型清除
下例中T是無邊界的,編譯時會被替換成Object
// Counts the number of occurrences of elem in anArray.
//
public static <T> int count(T[] anArray, T elem) {
    int cnt = 0;
    for (T e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}
T被替換成Object類
public static int count(Object[] anArray, Object elem) {
    int cnt = 0;
    for (Object e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}
如以下的繼承關係
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
我們可以寫一個泛型方法畫不同的形狀
public static <T extends Shape> void draw(T shape) { /* ... */ }
方法中的T會被替換成邊界類Shape
public static void draw(Shape shape) { /* ... */ }






















































發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章