Generic 範型 - type parameter

範型簡介

從JDK 5.0開始,範型作爲一種新的擴展被引入到了java語言中。

有了範型,我們可以對類型(type=class+interface)進行抽象。最常見的例子是容器類型。

List myIntList = new LinkedList(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = (Integer) myIntList.iterator().next(); // 3      

可以用範型對以上代碼進行優化:

List<Integer> 
    myIntList = new LinkedList<Integer>(); // 1'
myIntList.add(new Integer(0)); // 2'
Integer x = myIntList.iterator().next(); // 3'

優化帶來兩點改進:

  1. 省去了造型(cast)的麻煩。
  2. 除了代碼上的整潔,範型還在compile-time保證了代碼的類型正確。如果沒有範型,無法保證放入list的對象是Integer型。

定義簡單的範型

從package java.util中摘錄下接口List和Iterator的定義:

public interface List <E> {
    void add(E x);
    Iterator<E> iterator();
}

public interface Iterator<E> {
    E next();
    boolean hasNext();
}

這裏聲明瞭type parameter:E。Type parameters在範型的全部聲明中都可以用,就像使用其他普通的類型一樣。

調用範型的時候,需要爲type parameter E 指定一個真實的類型變量【1】(又稱爲parameterized type),例如:

List<Integer> myIntList = new LinkedList<Integer>();

可以想象List<Integer>是List的一個版本,在這個版本里面,所有的 type parameter (E)都被Integer替換了:

public interface IntegerList {
    void add(Integer x);
    Iterator<Integer> iterator();
}

這種想象很有幫助,因爲parameterized type的List<Integer> 確實包含了類似的方法;但是也容易帶來誤導,因爲每次調用範型並不會生成代碼的一個拷貝,通過編譯,一個範型類型的聲明只會編譯一次,生成一個class文件;每次調用範型,類似於給一個方法傳入了一個argument,只是這裏傳入的是一個普通的類型。

【1】這裏用的是argument,即傳給方法的值;區別parameter,parameter是作爲方法簽名的一部分,用於定義方法。

範型和子類型

假設Foo是Bar的子類型(class或者interface),G是一個範型類型聲明,G<Foo> 不是G<Bar>的子類型,這點有些反直覺。

wildcards 通配符

接着上一節的討論,假設Foo是Bar的子類型(class或者interface),G是一個範型類型聲明,G<Foo> 不是G<Bar>的子類型。可是,如果我們確實需要在G<Foo> 和G<Bar>之間建立父子關係呢?具體來說,假設有以下一段代碼:

void printCollection(Collection c) {
    Iterator i = c.iterator();
    for (k = 0; k < c.size(); k++) {
        System.out.println(i.next());
    }
}

用範型對其進行優化,這裏是一種錯誤的方式:

void printCollection(Collection<Object> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

這樣寫本身沒有錯誤,但是他對Collection中元素的類型進行了限制,只能是Object!那麼,所有collection的超類是神馬呢?就是Collection<?>(讀作"collection of unknown"),這個Collection的元素類型可以任意匹配,被稱作wildcard type

void printCollection(Collection<?> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

嗯,不錯,現在我們可以從c中讀取出任意類型的元素。可以,這樣一來,又出現了新的問題:什麼樣的元素可以放到c裏面去呢?答案是:任何類型的元素都無法放到c裏面去!因爲無法知道c中的type parameter(也許寫作E)是什麼類型。

Bounded Wildcards 有界通配符

可能是考慮到?過於寬泛,java引入了Bounded Wildcards,有界通配符。假設有以下代碼:

public abstract class Shape {
    public abstract void draw(Canvas c);
}

public class Circle extends Shape {
    private int x, y, radius;
    public void draw(Canvas c) {
        ...
    }
}

public class Rectangle extends Shape {
    private int x, y, width, height;
    public void draw(Canvas c) {
        ...
    }
}

// These classes can be drawn on a canvas:
public class Canvas {
    public void draw(Shape s) {
        s.draw(this);
   }
}
// Assuming that they are represented as a list, 
// it would be convenient to have a method in Canvas that draws them all:
public void drawAll(List<Shape> shapes) {
    for (Shape s: shapes) {
        s.draw(this);
   }
}

看上去不錯,但是問題又來了,類型方法drawAll的簽名參數中的ShapeCircle的超類,儘管CircleShape的子類,但是List<Circle>不是List<Shape>的子類。所以要想drawAll可以處理List<Circle>,可以將其定義爲:

public void drawAll(List<? extends Shape> shapes) {
    ...
}

Bounded Wildcards 也面臨着?面臨的問題,那就是他們都過於寬泛,因此無法
確定什麼樣的元素可以放到集合裏面:

public void addRectangle(List<? extends Shape> shapes) {
    // Compile-time error!
    shapes.add(0, new Rectangle());
}

範型方法

前面討論了範型type的聲明,其實,同樣可以聲明範型方法:

static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
    for (T o : a) {
        c.add(o); // Correct
    }
}

所謂範型方法,就是在方法簽名內的修飾符和方法返回類型之間,加入了type parameter,例如<T>。在調用方法的時候,並不需要傳入type argument,編譯器會根據actual argument的類型推斷(infer)出type argument。

較之於範型類型,範型方法的聲明要稍微複雜一些。具體來說,範型方法包含返回值和若干parameter,而他們之間可能會存在着類型的依賴關係。而這種依賴關係就帶來一個問題,什麼時候應該使用通配符,什麼時候應該使用範型方法呢?
比如,查看JDK文檔:

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

爲什麼不寫成:

interface Collection<E> {
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    // Hey, type variables can have bounds too!
}

在containsAll 和 addAll中,type parameter T 僅使用了1次。返回值和其他parameter並不依賴於它,這種情況下,應該使用通配符。只有當返回值和parameter之間存在依賴的情況下,才應該使用範型方法。例如:

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

範型是如何實現的

範型是通過編譯器對代碼的erasure轉換實現的。可以把這一過程想象成source-to-source的翻譯。例如:

public String loophole(Integer x) {
    List<String> ys = new LinkedList<String>();
    List xs = ys;
    xs.add(x); // Compile-time unchecked warning
    return ys.iterator().next();
}

將被翻譯成:

public String loophole(Integer x) {
    List ys = new LinkedList;
    List xs = ys;
    xs.add(x); 
    return(String) ys.iterator().next(); // run time error
}

在第二段代碼中,我們從list中取出一個元素,並試圖通過將其cast(造型)把它當成String處理,這裏會得到一個ClassCastException。

因爲在編譯階段,編譯器對代碼進行了erasure,<>內的一切都被刪除了,所以所有對範型類型的調用(Invocations,或者說實例)共享同一個run-time class,隨之而來的,static變量和方法也被這些實例共享,所以在static方法中,也無法引用type parameter;同時,Cast 和InstanceOf操作也就都失去了意義。

Collection cs = new ArrayList<String>();
// Illegal.
if (cs instanceof Collection<String>) { ... }

// Unchecked warning,
Collection<String> cstr = (Collection<String>) cs;
//gives an unchecked warning, since this isn't something the runtime system is //going to check for you.

同理,對於方法來說,type variables(<T>在方法中叫type variables,在類型聲明中叫parameterized type)也不存在於run-time:

// Unchecked warning. 
<T> T badCast(T t, Object o) {
    return (T) o;
}

如何定義範型數組

private E[] elements = (E[]) new Object[10];
  • 數組和範型對類型的檢查是不同的。

對於數組來說,下面的語句是合法的:

Object[] arr = new String[10];

Object[] 是 String[]的超類,因爲Object是String的超類。然而,對於範型來說,就沒有這樣的繼承關係,因此,以下聲明無法通過編譯:

List<Object> list = new ArrayList<String>(); // Will not compile. generics are invariant.

java中引入範型,是爲了在編譯階段強化類型檢查。同時,因爲type erasure,範型也沒有runtime的任何信息。所以,List<String> 只有靜態類型的 List<String>,和一個動態類型 List

但是,數組攜帶了runtime的類型信息。在runtime,數組用Array Store Check來檢查將要插入的元素是否和真實的數組類型兼容。因此,以下代碼能很好的編譯,但是由於Array Store Check,會在runtime失敗:

Object[] arr = new String[10];
arr[0] = new Integer(10);

回到範型,編譯器會提供編譯階段的檢查,避免這種以這種方式創建索引,防止runtime的異常出現。

  • 那麼,創建範型數組有什麼問題呢?
    創建元素的類型是type parameter, parameterized type 或者bounded wildcard parameterized type的數組是type-unsafe的。考慮如下代碼:
public <T> T[] getArray(int size) {
    T[] arr = new T[size];  // Suppose this was allowed for the time being.
    return arr;
}

在rumtime,T的類型未知,實際上創建的數組是Object[],因此在runtime,上面的方法像是:

public Object[] getArray(int size) {
    Object[] arr = new Object[size];
    return arr;
}

假設,有以下調用:

Integer[] arr = getArray(10);

這就是問題,這裏將Object[] 指派給了一個Integer[]類型的索引,這段代碼編譯沒有問題,但是在runtime會失敗。因此,創建範型數組是不合法的。

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