Java 集合框架(如 Vector、ArrayList 等)在設計上為了實現「通用性」,犧牲了編譯期的類型安全。當你將對象放入集合時,集合只保存 Object 類型的引用。這意味著編譯器不再知道集合裡具體存放的是什麼類型的對象。
早期 Java 集合的設計者無法預知使用者想存放何種類型。若強制限定集合只能存特定類型,將嚴重限制其通用性。因此,集合統一持有 Object 引用——Java 所有類的共同祖先。這雖然解決了通用性問題,但引入了兩大隱患:
Object。Object 引用,必須手動轉型為目標類型(如 (Cat)cats.elementAt(i))。若類型不匹配,運行期會拋出 ClassCastException。雖然編譯期無法攔截類型錯誤,但 Java 在運行期會嚴格檢查轉型操作。若你將「狗」誤放入「貓」集合,並在取出時嘗試轉為 Cat,JVM 會立即拋出異常,防止錯誤繼續蔓延。這比 C++ 的未定義行為安全得多。
//: CatsAndDogs.java
import java.util.*;
class Cat {
private int catNumber;
Cat(int i) { catNumber = i; }
void print() { System.out.println("Cat #" + catNumber); }
}
class Dog {
private int dogNumber;
Dog(int i) { dogNumber = i; }
void print() { System.out.println("Dog #" + dogNumber); }
}
public class CatsAndDogs {
public static void main(String[] args) {
Vector cats = new Vector();
for (int i = 0; i < 7; i++)
cats.addElement(new Cat(i));
// 編譯器不阻止你加入 Dog!
cats.addElement(new Dog(7));
for (int i = 0; i < cats.size(); i++)
((Cat)cats.elementAt(i)).print(); // 運行期這裡會拋出異常
}
}
///:~
有兩種情況看起來不需要顯式轉型:
String 但得到 Object 時,會自動調用 toString()。這只對 String 有效。Object,內部再轉型。雖然推遲了檢查,但依然能在運行期捕獲錯誤。在泛型(Java 5 引入)之前,常見做法是創建針對特定類型的包裝類,如 GopherVector:
class GopherVector {
private Vector v = new Vector();
public void addElement(Gopher m) { v.addElement(m); }
public Gopher elementAt(int index) { return (Gopher)v.elementAt(index); }
public int size() { return v.size(); }
}
這樣編譯期就能保證類型安全,取出元素時也無需手動轉型。
這類問題的根本解決之道是「參數化類型」(即泛型)。C++ 早已通過模板支持,Java 當時保留了 generic 關鍵字等待未來。最終在 Java 5 中,泛型正式引入,徹底解決了集合類型不安全的問題。