Java基礎學習——泛型(generics)二

通配符(Wildcard)

考慮一個打印集合內所有元素的問題。下面這個可能是在Java舊版本中的寫法:

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

下面是一個用泛型的天真嘗試(同時使用foreach語法):

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

問題是新版本並不比舊版本更有益處。而舊版代碼可以使用任何類型集合當參數傳入,而新的代碼確只能接受* Collection<Object>*類型,正如上文介紹,Collection<Object>並不是其他任何泛型集合的父類型。

那麼什麼纔是所有泛型集合的父類型呢?那就是被寫成Collection<?>(表示“未知類型的集合” “collection of unknown”)的集合,一種元素類型可以匹配任何類型的集合。這就是它被叫做通配類型(wildcard type)的直白原因。我們可以這樣寫:

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

現在,我們可以用任何泛型集合當參數調用了。注意一下,在printCollection函數內部,我們依然是用Object類型去讀取集合內的元素。無論集合內真實類型是什麼,這樣做是安全的,因爲集合真的包含了Object。但是隨意添加Object對象卻不一定是安全的。

Collection<?> c = new ArrayList<String>();
c.add(new Object()); // Compile time error

因爲我們不知道c的元素類型時,我們是不能往裏面添加對象的。方法add需要一個E類型的參數,E表示集合元素類型。什麼時候實際類型參數是?呢?它代表着一些未知的類型(unknown type)。我們傳入add方法的參數都必須是未知類型的子類型。因爲我們不知道未知類型是什麼,所以我們不能傳入任何東西。唯一的例外就是null,null是所有類型的成員。

另外一方面,給定List<?>,我們能夠調用get()方法,並且利用返回值。返回值是一個未知類型,但我們總是能知道它是一個對象。因此,我們總是能安全地將get()的結果賦值給一個Object類型變量或者將它用在任何期望Object類型的地方。

受限通配符(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) {
        ...
    }
}

這些類都可以在畫布上繪出:

public class Canvas {
    public void draw(Shape s) {
        s.draw(this);
   }
}

一個圖紙總是會包含很多形狀。假設圖紙用一個list表示,有個方面的函數Canvas可以畫出所有圖形:

public void drawAll(List<Shape> shapes) {
    for (Shape s: shapes) {
        s.draw(this);
   }
}

現在,類型規則說明drawAll()函數只能以Shape的list爲參數被調用。它實際上被不能以List<Circle>爲參數。這是不幸的,因爲所有方法做的就是從List裏讀取shape,所以,它也應該能接受List<Circle>爲參數。其實,我們真實的想法,是接受一個shape子類類型的list:

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

上面的代碼有一個很小卻很重要的區別:我們用List

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

你應該能指出上面的代碼爲啥不能被允許。因爲add方法的第二個參數類型是? extends Shape——Shape的未知子類型。因爲我們不知道類型是啥,我們並不知道它是不是Rectangle的父類型。它可能是,也可能不是一個超類型,所以,這個地方傳入Rectangle是不安全的。

受限通配符(Bounded wildcards)正好解決之前那個從人口普查局(the census bureau)傳送數據給DMV的例子。之前的例子假設,數據是存放在map裏的,用人名作key,人員信息(可以用Person類或其子類,比如Driver,代表)爲value。Map<K,V>是一個有兩個類型參數的泛型例子,兩個參數代表map的key與value。

再次提醒,參數類型的命名慣例是:K表示key,V表示values。

public class Census {
    public static void addRegistry(Map<String, ? extends Person> registry) {
}
...

Map<String, Driver> allDrivers = ... ;
Census.addRegistry(allDrivers);

以上內容翻譯自Java 官網泛型教程之通配符

泛型方法(Generic Methods)

考慮一下,需要寫個將一個Object數組的元素都寫入另外一個collection(集合)的方法。下面是第一次嘗試:

static void fromArrayToCollection(Object[] a, Collection<?> c) {
    for (Object o : a) { 
        // 編譯錯誤
        c.add(o); // compile-time error
    }
}

到目前爲止,你可能已經學會避免剛開始時用Collection<Object>來代替所有集合的錯誤。你可能已經認識到,也可能還沒有,Collection

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

我們可以用任何類型的集合調用此函數,只要集合的元素類型是數組元素類型的父類型。

Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();

// T inferred to be Object
// 將T自動推導爲Object
fromArrayToCollection(oa, co); 

String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();

// T inferred to be String
// 將T自動推導爲String
fromArrayToCollection(sa, cs);

// T inferred to be Object
// T 自動推導爲Object
fromArrayToCollection(sa, co);

Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();

// T inferred to be Number
// T 自動推導爲Number
fromArrayToCollection(ia, cn);

// T inferred to be Number
// T 自動推導爲Number
fromArrayToCollection(fa, cn);

// T inferred to be Number
// T 自動推導爲Number
fromArrayToCollection(na, cn);

// T inferred to be Object
// T 自動推導爲Object
fromArrayToCollection(na, co);

// compile-time error
// 編譯錯誤
fromArrayToCollection(na, cs);

注意一下,我們並沒有直接傳入類型給泛型函數。Java編譯器會根據實際參數的類型自動推導T的類型。

但是有一個問題:什麼時候我們需要泛型函數,什麼時候我們需要通配符類型。爲了搞清楚這個問題。我們拿出幾個jdk裏的Collection類方法作爲例子。

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和addAl函數裏,類型參數T都僅僅被用過一次。返回值既不依賴類型參數,也沒有其他參數依賴此類型(在這個例子裏,方法只有一個參數)。這便是告訴我們這裏的類型參數只是用於多態,僅僅爲了可以讓方法被各種各樣的參數類型調用。如果是上面這種情況,就是應該使用通配符(wildcards)。通配符正是設計用來支持靈活的子類型,像上面的例子那樣。

泛型方法用來使類型參數可以體現多個參數之間或參數與返回值之間的類型依賴關係。如果沒有這些依賴關係,則不應該使用泛型方法。

泛型方法和通配符是可以串聯使用的。比如Collections.copy():

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

注意一下,上面的依賴關係是兩個參數類型之間的依賴。list src裏的元素都必須可以賦值給dst list的元素類型T。所以,src的元素類型可以是T的任何類型,我們也不關心具體是哪個子類型。copy函數的聲明體現了類型參數的依賴,並且在第二個參數的類型使用了通配符。

我們也可以用完全不用通配符的方式,改寫上面函數的聲明:

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

改寫後,第一個參數類型,既用在了dst,同時又是第二個類型參數S的上界,同時,S僅僅只在src裏用了一次。這便是用通配符替換S的標誌。這裏,使用通配符則比類型參數更加簡潔清晰。因此,如果合適,則應首選通配符。

通配符同時還有其他優點,它可以在方法外面被使用,比如成員變量,局部變量和數組等。這裏有個例子。
回到之前的繪畫程序的例子。假設我們想保留繪畫的歷史操作。我們可以使用一個靜態變量保留歷史,然後,在drawAll()函數中,將操作保存到歷史靜態變量裏。

static List<List<? extends Shape>> 
    history = new ArrayList<List<? extends Shape>>();

public void drawAll(List<? extends Shape> shapes) {
    history.addLast(shapes);
    for (Shape s: shapes) {
        s.draw(this);
    }
}

在結束前,讓我們再次強調下,類型參數的命名約定。當我們沒有更加詳細內容來說明的類型,我們使用T代表類型。在泛型函數裏,經常出現這種情況。如果有多個參數類型,我們就是T旁邊的字面,比如S。如果一個泛型方法出現在泛型類(generic class)裏,爲了避免混淆,最好不要使用相同的類型參數名稱。這也使用於泛型嵌套類。

以上內容翻譯自 Java 官網

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