1. 程式人生 > >Effective Java 第三版——29. 優先考慮泛型

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

調用 system correct temp per except 語言 程序員 但是

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

技術分享圖片

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數組並將其轉換為泛型數組類型。 現在沒有了錯誤,編譯器會發出警告。 這種用法是合法的,但不是(一般)類型安全的:

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)。

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