Java泛型——擦除
本章涉及到:
- 擦除的效果
- 擦出後編譯器保證型別的正確性
- 擦除的由來
需要了解的朋友可以看一下。
這篇文章先對比了一下C++的泛型程式碼,能讓你更清楚的感受到擦除的效果(請放心,只是簡單的c++程式碼,不瞭解c++的同學也能看的懂)。
先看下C++的泛型:
#include <iostream>
using namespace std;
template <class T> class Manipulator{
T obj;
public:Manipulator(T x){ obj=x; }
void manipulate(){ obj.f(); }
};
class HasF{
public:void f(){ cout<<"HasF::f()"<< endl; }
};
int main(){
HasF hf;
Manipulator<HasF> manipulator(hf);
manipulator.manipulate();
}
/*Output: HasF::f() */
Manipulator類儲存了一個型別T的物件,有意思的地方是manipulate()方法,這個方法中obj呼叫了方法f()。在C++中當你例項化這個模板時,C++編譯器將進行檢查,因此在Manipulator<HasF>被例項化的這一刻,它看到HasF擁有一個方法f()。如果情況並非如此,就會得到一個編譯期的錯誤,這樣型別安全就得到保障了。 ——————以上摘自Thinking in Java (事實上這篇文章大部分內容都會摘自Thinking in Java >.<! )
接下來看一下Java中的程式碼:
publicclass HasF {
public void f() {System.out.println("HasF.f()");}}
}
publicclass Manipulator<T> {
private T obj;
public Manipulator(T obj) {
this.obj = obj;
}
public void manipulate() {
// obj.f();//此處報錯
}
}
public class Manipulation {
public static void main(String[] args) {
HasF hasF = new HasF();
Manipulator<HasF> manipulator = new Manipulation(hasF);
manipulator.manipulate();
}
}
可以看出Java和C++泛型程式碼很明顯區別就是C++中的泛型有實際型別資訊,而Java中的實際型別資訊不管是編譯時期還是在執行時期都被擦除了,這就是擦除的效果。由於有了擦除,Java編譯器無法將obj呼叫f()這一需求對映到HasF擁有f()這一事實上。(事實上擦除是將泛型型別資訊擦除到了它的第一個邊界,預設不設定的邊界是Object,你可以呼叫Object的方法,可以這樣設定邊界——<T extends HasF>,設定邊界後就可以呼叫f()了。這篇文章不講邊界的概念)
通過上面的對比應該能明顯的感受到Java擦除的存在了吧?在Java中,當你使用泛型時,任何具體的型別資訊都會被擦除,你唯一知道的就是你在使用一個物件。
那這樣問題就來了——實際的泛型型別資訊被擦除了,Java是怎麼保障型別資訊的正確性呢?看下一段Java程式碼:
public class FilledListMaker<T> {
List<T> create(T t, int n) {
List<T> result = new ArrayList<T>();
for (int i = 0; i < n; i++) {publicresult.add(t);}
}
public static void main(String[] args) {
FilledListMaker<String> stringMaker = new FilledListMaker<String>();
List<String> list = stringMaker.create("hello", 4);
System.out.println(list);
//System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
}
}
/*[hello,hello,hello,hello]/*
/* [E] /*
在這段程式碼中看起來好像是擁有了String引數型別資訊,但實際並非如此。最後一行程式碼的意思是輸出一個TypeVariable物件陣列,陣列表示泛型所宣告的型別資訊。但是,正如你看見的,輸出的只是用作引數佔位符的識別符號,並非有用的資訊。
在這上面這段程式碼中編譯器無法知道有關create()中T的任何資訊,但是它仍舊可以在編譯期確保你放置到result中的物件具有T型別,使其適合ArrayList<T>。因此,即使擦除在方法或類內部移除了有關實際型別的資訊,編譯器仍舊可以確保在方法或類中使用的型別的內部一致性。(記住編譯器無法知道型別資訊,但它可以保障型別資訊保安)
擦除在方法體中移除了型別資訊,所以在執行時的問題就是邊界,即物件進入和離開方法的地點。這些正是編譯器在編譯期執行型別檢查並插入轉型程式碼的地點。(這段話中的邊界和上方設定邊界不是一個概念,別搞混了。)再看看下面的兩段程式碼:
public class SimpleHolder {
private Object obj;
public Object getObj() { return obj; }
public void setObj(Object obj) { this.obj = obj; }
public static void main(String[] args) {
SimpleHolder simpleHolder = new SimpleHolder();
simpleHolder.setObj("Item");
String s = (String) simpleHolder.getObj();
}
}
public class GenericHolder<T>{
private T obj;
public T getObj() { return obj; }
public void setObj(T obj) { this.obj = obj; }
public static void main(String[] args) {
GenericHolder genericHolder = new GenericHolder();
genericHolder.setObj("Item");
String s = genericHolder.getObj();
}
}
如果用Java -c SimpleHolder反編譯這個類,就可以得到下面的內容(直接上圖了實在是懶得寫了):
SimpleHolder
可以看出set()和get()方法直接儲存和產生值,而轉型是在呼叫get()的時候接受檢查的。接下來看一下GenericHolder:
GenericHolder
可以看出GenericHolder產生的位元組碼操作和SimpleHolder產生的位元組碼操作是相同的。對於進入set()的型別進行檢查是不需要的,因為這將由編譯器執行。而對從get()返回的值進行轉型仍舊是需要的,但這與你自己必須執行的操作是一樣的——此處它將由編譯器自動插入。
那麼問題又來了——為什麼要有擦除?
擦除的核心動機是它使得泛化的客戶端可以用非泛化的類庫來使用,反之亦然,這經常被稱為“遷移相容性”。(因為泛型的概念是JavaSE5之後提出的,所以為了相容以前的客戶端和類庫提出了擦除的概念,擦除也使得非泛化程式碼向著泛型的遷移成為了可能。)
擦除存在的主要理由是從非泛化程式碼到泛化程式碼的轉變過程,以及在不破壞現有類庫的情況下,將泛型融入Java語言中。擦除使得現有的非泛型客戶端程式碼能夠在不改變的情況下繼續使用,直至客戶端準備好用泛型重寫這些程式碼。擦除的代價也是比較明顯的,實際型別的資訊丟失,無法使用轉型、instanceof操作、和new表示式。當你在編寫泛型程式碼時,必須要提醒自己你只是看起來好像擁有了有關引數型別資訊而已,實際上這個泛型型別的實際型別資訊將被擦除到第一個邊界,你唯一知道的就是你只是在操作一個物件。
那麼總結一下,擦除是擦掉了泛型型別的實際型別資訊(擦除到了第一個邊界),而編譯器保障了型別的安全性,擦除的存在是為了實現遷移的相容性。
有需要改進的地方,望指出,謝謝。