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>
)要求實際的類型參數E
是java.util.concurrent.Delayed
的子類型。 這使得DelayQueue
實現及其客戶端可以利用DelayQueue
元素上的Delayed
方法,而不需要顯式的轉換或ClassCastException異常的風險。 類型參數E被稱為限定類型參數。 請注意,子類型關系被定義為每個類型都是自己的子類型[JLS,4.10],因此創建DelayQueue <Delayed>
是合法的。
總之,泛型類型比需要在客戶端代碼中強制轉換的類型更安全,更易於使用。 當你設計新的類型時,確保它們可以在沒有這種強制轉換的情況下使用。 這通常意味着使類型泛型化。 如果你有任何現有的類型,應該是泛型的但實際上卻不是,那么把它們泛型化。 這使這些類型的新用戶的使用更容易,而不會破壞現有的客戶端(條目 26)。