Effective Java 第三版——29. 優先考慮泛型

Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必不少人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到如今已經將近8年的時間,但隨着Java 6,7,8,甚至9的發佈,Java語言發生了深入的變化。
在這裏第一時間翻譯成中文版。供你們學習分享之用。java

Effective Java, Third Edition

29. 優先考慮泛型

參數化聲明並使用JDK提供的泛型類型和方法一般不會太困難。 但編寫本身的泛型類型有點困難,但值得努力學習。程序員

考慮條目 7中的簡單堆棧實現:數組

// Object-based collection - a prime candidate for generics
public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null; // Eliminate obsolete reference
        return result;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

這個類應該已經被參數化了,可是因爲事實並不是如此,咱們能夠對它進行泛型化。 換句話說,咱們能夠參數化它,而不會損害原始非參數化版本的客戶端。 就目前而言,客戶端必須強制轉換從堆棧中彈出的對象,而這些強制轉換可能會在運行時失敗。 泛型化類的第一步是在其聲明中添加一個或多個類型參數。 在這種狀況下,有一個類型參數,表示堆棧的元素類型,這個類型參數的常規名稱是E(條目 68)。安全

下一步是用相應的類型參數替換全部使用的Object類型,而後嘗試編譯生成的程序:性能

// Initial attempt to generify Stack - won't compile!
public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new E[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public E pop() {
        if (size == 0)
            throw new EmptyStackException();
        E result = elements[--size];
        elements[size] = null; // Eliminate obsolete reference
        return result;
    }
    ... // no changes in isEmpty or ensureCapacity
}

你一般會獲得至少一個錯誤或警告,這個類也不例外。 幸運的是,這個類只產生一個錯誤:學習

Stack.java:8: generic array creation
        elements = new E[DEFAULT_INITIAL_CAPACITY];
                   ^

如條目 28所述,你不能建立一個不可具體化類型的數組,例如類型E。每當編寫一個由數組支持的泛型時,就會出現此問題。 有兩種合理的方法來解決它。 第一種解決方案直接規避了對泛型數組建立的禁用:建立一個Object數組並將其轉換爲泛型數組類型。 如今沒有了錯誤,編譯器會發出警告。 這種用法是合法的,但不是(通常)類型安全的:ui

Stack.java:8: warning: [unchecked] unchecked cast
found: Object[], required: E[]
        elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
                       ^

編譯器可能沒法證實你的程序是類型安全的,但你能夠。 你必須說服本身,不加限制的類型強制轉換不會損害程序的類型安全。 有問題的數組(元素)保存在一個私有屬性中,永遠不會返回給客戶端或傳遞給任何其餘方法。 保存在數組中的惟一元素是那些傳遞給push方法的元素,它們是E類型的,因此未經檢查的強制轉換不會形成任何傷害。命令行

一旦證實未經檢查的強制轉換是安全的,請儘量縮小範圍(條目 27)。 在這種狀況下,構造方法只包含未經檢查的數組建立,因此在整個構造方法中抑制警告是合適的。 經過添加一個註解來執行此操做,Stack能夠乾淨地編譯,而且能夠在沒有顯式強制轉換或擔憂ClassCastException異常的狀況下使用它:翻譯

// The elements array will contain only E instances from push(E).
// This is sufficient to ensure type safety, but the runtime
// type of the array won't be E[]; it will always be Object[]!
@SuppressWarnings("unchecked")
public Stack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}

消除Stack中的泛型數組建立錯誤的第二種方法是將屬性元素的類型從E []更改成Object []。 若是這樣作,會獲得一個不一樣的錯誤:設計

Stack.java:19: incompatible types
found: Object, required: E
        E result = elements[--size];
                           ^

能夠經過將從數組中檢索到的元素轉換爲E來將此錯誤更改成警告:

Stack.java:19: warning: [unchecked] unchecked cast
found: Object, required: E
        E result = (E) elements[--size];
                               ^

由於E是不可具體化的類型,編譯器沒法在運行時檢查強制轉換。 再一次,你能夠很容易地向本身證實,不加限制的轉換是安全的,因此能夠適當地抑制警告。 根據條目 27的建議,咱們只在包含未經檢查的強制轉換的分配上抑制警告,而不是在整個pop方法上:

// Appropriate suppression of unchecked warning
public E pop() {
    if (size == 0)
        throw new EmptyStackException();

    // push requires elements to be of type E, so cast is correct
    @SuppressWarnings("unchecked") E result =
        (E) elements[--size];

    elements[size] = null; // Eliminate obsolete reference
    return result;
}

兩種消除泛型數組建立的技術都有其追隨者。 第一個更可讀:數組被聲明爲E []類型,清楚地代表它只包含E實例。 它也更簡潔:在一個典型的泛型類中,你從代碼中的許多點讀取數組; 第一種技術只須要一次轉換(建立數組的地方),而第二種技術每次讀取數組元素都須要單獨轉換。 所以,第一種技術是優選的而且在實踐中更經常使用。 可是,它確實會形成堆污染(heap pollution)(條目 32):數組的運行時類型與編譯時類型不匹配(除非E碰巧是Object)。 這使得一些程序員很是不安,他們選擇了第二種技術,儘管在這種狀況下堆的污染是無害的。

下面的程序演示了泛型Stack類的使用。 該程序以相反的順序打印其命令行參數,並將其轉換爲大寫。 對從堆棧彈出的元素調用String的toUpperCase方法不須要顯式強制轉換,而自動生成的強制轉換將保證成功:

// Little program to exercise our generic Stack
public static void main(String[] args) {
    Stack<String> stack = new Stack<>();
    for (String arg : args)
        stack.push(arg);
    while (!stack.isEmpty())
        System.out.println(stack.pop().toUpperCase());
}

上面的例子彷佛與條目 28相矛盾,條目 28中鼓勵使用列表優先於數組。 在泛型類型中使用列表並不老是可行或可取的。 Java自己生來並不支持列表,因此一些泛型類型(如ArrayList)必須在數組上實現。 其餘的泛型類型,好比HashMap,是爲了提升性能而實現的。

絕大多數泛型類型就像咱們的Stack示例同樣,它們的類型參數沒有限制:能夠建立一個Stack <Object>Stack <int []>Stack <List <String >>或者其餘任何對象的Stack引用類型。 請注意,不能建立基本類型的堆棧:嘗試建立Stack<int>Stack<double>將致使編譯時錯誤。 這是Java泛型類型系統的一個基本限制。 可使用基本類型的包裝類(條目 61)來解決這個限制。

有一些泛型類型限制了它們類型參數的容許值。 例如,考慮java.util.concurrent.DelayQueue,它的聲明以下所示:

class DelayQueue<E extends Delayed> implements BlockingQueue<E>

類型參數列表(<E extends Delayed>)要求實際的類型參數Ejava.util.concurrent.Delayed的子類型。 這使得DelayQueue實現及其客戶端能夠利用DelayQueue元素上的Delayed方法,而不須要顯式的轉換或ClassCastException異常的風險。 類型參數E被稱爲限定類型參數。 請注意,子類型關係被定義爲每一個類型都是本身的子類型[JLS,4.10],所以建立DelayQueue <Delayed>是合法的。

總之,泛型類型比須要在客戶端代碼中強制轉換的類型更安全,更易於使用。 當你設計新的類型時,確保它們能夠在沒有這種強制轉換的狀況下使用。 這一般意味着使類型泛型化。 若是你有任何現有的類型,應該是泛型的但實際上卻不是,那麼把它們泛型化。 這使這些類型的新用戶的使用更容易,而不會破壞現有的客戶端(條目 26)。

相關文章
相關標籤/搜索