如下代碼編譯無法通過:
class A{} class B extends A {} public static void funC(List<A> listA) { // ... } public static void funD(List<B> listB) { funC(listB); // ... }
Unresolved compilation problem: The method doPrint(List<A>) in the type test is not applicable for the arguments (List<B>)
而下面的代碼就沒問題:
public static void funC(A a) { // ... } public static void funD(B b) { funC(b); // ... }
在第二段代碼中,類型B的實例向上轉換成類型A的實例傳入函數funC(A a),這是正常的隱式類型轉換。而第一段代碼則表明類型List<B>的實例無法轉換成類型List<A>的實例。這就引出了Java泛型的類型轉換問題。
Java是在JDK 5中引入的泛型(generics)新特性的。最主要的應用是在JDK 5中的新集合類框架中。可以解決之前的集合類框架在使用過程中出現的運行時刻類型錯誤,把問題暴露在編譯中。但是為了保證與舊版本的兼容性,Java泛型的實現采用了類型擦除的機制,帶來了一些缺陷。Java泛型的分析參考:http://www.infoq.com/cn/articles/cf-java-generics/。
Java泛型的實現:類型擦除
以上問題在C++中不會出現,這是兩種語言的泛型實現機制不同造成的。
C++中的泛型類或方法,編譯器會自動檢查實例化的參數類型,然后根據類型檢查參數類型的調用是否合理。如下代碼能很好說明這個問題,模板在定義時並不對參數類型T的操作提前限制,等模板實例化時會根據T的具體類型檢查操作是否合理。
template<class T> class Manipulator { T obj; public: Manipulator(T x) {obj = x; } void manipulate() {obj.f(); } }; class HasF { void f() { // ... } }; int main() { HasF hf; Manipulator<HasF> manipulator(hf); manipulator.manipulate(); }
Java采用的是所謂的“擦除”機制。在編譯時不考慮傳入的類型,把該類型的印跡全部擦除,視該類型為最基本的Object類型。但是Java會在編譯時檢查泛型接口傳入的類型參數的實例是否存在隱含的轉換情況,為了安全,泛型禁止此類轉換。這就是開始我們遇到的問題,編譯器檢測到需要把List<B>實例轉換成List<A>實例,這是被禁止的。明明可以實現的類型轉換卻被禁止,原因是編譯時把類型參數擦除,當做Object,在運行時還要把Object類型的實例轉換成類型參數的實例,兩次轉換存在運行時錯誤的風險。
為了解決泛型中隱含的轉換問題,Java泛型加入了類型參數的上下邊界機制。<? extends A>表示該類型參數可以是A(上邊界)或者A的子類類型。編譯時擦除到類型A,即用A類型代替類型參數。這種方法可以解決開始遇到的問題,編譯器知道類型參數的范圍,如果傳入的實例類型B是在這個范圍內的話允許轉換,這時只要一次類型轉換就可以了,運行時會把對象當做A的實例看待。
引入上邊界使Java泛型具有了像C++泛型那樣在模板中調用類型參數的方法的能力。如下Java代碼,實現了上面C++代碼同樣的功能:
class Manipulator<T extends hasF> { T hf; public: Manipulator(T t) {hf = t; } void manipulate() {hf.f(); } };