謎題89:泛型迷藥
和前一個謎題一樣,本謎題也大量使用了泛型。我們從前面的錯誤中吸取教訓,這次不再使用原生類型了。這個程序實現了一個簡單的鏈表數據結構。main程序構建了一個包含2個元素的list,然後輸出它的內容。那麽,這個程序會打印出什麽呢?
public class LinkedList<E> {
private Node<E> head = null;
private class Node<E> {
E value;
Node<E> next;
// Node constructor links the node as a new head
Node(E value) {
this.value = value;
this.next = head;
head = this;
}
}
public void add(E e) {
new Node<E>(e);
// Link node as new head
}
public void dump() {
for (Node<E> n = head; n != null; n = n.next)
System.out.println(n.value + " ");
}
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<String>();
list.add("world");
list.add("Hello");
list.dump();
}
}
又是一個看上去相當簡單的程序。新元素被添加到鏈表的表頭,而dump方法也是從表頭開始打印list。因此,元素的打印順序正好和它們被添加到鏈表中的順序相反。在本例中,程序先添加了“world”然後添加了“Hello”,所以總體來看它似乎就是一個復雜化的Hello World程序。遺憾的是,如果你嘗試著編譯它,就會發現它不能通過編譯。編譯器的錯誤消息是令人完全無法理解的:
LinkedList.java:11: incompatible types
found : LinkedList<E>.Node<E>
required: LinkedList<E>.Node<E>
this.next = head;
^
LinkedList.java:12: incompatible types
found : LinkedList<E>.Node<E>
required: LinkedList<E>.Node<E>
head = this;
^
編譯器試圖告訴我們,這個程序太過復雜了。一個泛型類的內部類可以訪問到它的外圍類的類型參數。而編程者的意圖很明顯,即一個Node的類型參數應該和它外圍的LinkedList類的類型參數一樣,所以Node完全不需要有自己的類型參數。要訂正這個程序,只需要去掉內部類的類型參數即可:
// 修復後的代碼,可以繼續修改得更好
public class LinkedList<E> {
private Node head = null;
private class Node {
E value;
Node next;
//Node的構造器,將node鏈接到鏈表上作為新的表頭
Node(E value) {
this.value = value;
this.next = head;
head = this;
}
}
public void add(E e) {
new Node(e);
//將node鏈接到鏈表上作為新的表頭
}
public void dump() {
for (Node n = head; n != null; n = n.next)
System.out.print(n.value + " ");
}
}
以上是解決問題的最簡單的修改方案,但不是最優的。最初的程序所使用的內部類並不是必需的。正如謎題80中提到的,你應該優先使用靜態成員類而不是非靜態成員類[EJ Item 18]。LinkedList.Node的一個實例不僅含有value和next域,還有一個隱藏的域,它包含了對外圍的LinkedList實例的引用。雖然外部類的實例在構造階段會被用來讀取和修改head,但是一旦構造完成,它就變成了一個甩不掉的包袱。更糟的是,這樣使得構造器中被置入了修改head的負作用,從而使程序變得難以讀懂。應該只在一個類自己的方法中修改該類的實例域。
因此,一個更好的修改方案是將最初的那個程序中對head的操作移到LinkedList.add方法中,這將會使Node成為一個靜態嵌套類而不是真正的內部類。靜態嵌套類不能訪問它的外圍類的類型參數,所以現在Node就必須有自己的類型參數了。修改後的程序既簡單清楚又正確無誤:
class LinkedList<E> {
private Node<E> head = null;
private static class Node<T> {
T value; Node<T> next;
Node(T value, Node<T> next) {
this.value = value;
this.next = next;
}
}
public void add(E e) {
head = new Node<E>(e, head);
}
public void dump() {
for (Node<E> n = head; n != null; n = n.next)
System.out.print(n.value + " ");
}
}
總之,泛型類的內部類可以訪問到其外圍類的類型參數,這可能會使得程序模糊難懂。本謎題所闡述的誤解對於初學泛型的程序員來說是普遍存在的。在一個泛型類中設置一個內部類並不是必錯的,但是很少用到這種情況,而且你應該考慮重構你的代碼來避免這種情況。當你在一個泛型類中嵌套另一個泛型類時,最好為它們的類型參數設置不同的名字,即使那個嵌套類是靜態的也應如此。對於語言設計者來說,或許應該考慮禁止類型參數的遮蔽機制,同樣的,局部變量的遮蔽機制也應該被禁止。這樣的規則就可以捕獲到本謎題中的錯誤了。
謎題89:泛型迷藥