在JAVA中,泛型只被JAVA編譯器支持,並不被JVM所支持,也就是說沒有定義新的字節碼來表示泛型類型,自然在JVM裡面也不會有新的指令來支持新的字節碼。類比到.NET來說,也就是被C#編譯器支持而不被CLR所支持。這樣就產生了很多有趣的問題。我們都知道我們的代碼都要經過編譯器的翻譯改動,JAVA中的泛型就是JAVA編譯器采用類型擦除的方式來實現泛型的。定義的泛型類型,都自動提供了一個相應的原始類型(raw type)原始類型的名字就是刪去類型參數後的泛型名,擦出掉類型變量,並替換為限定類型(無限定的變量用Object),可以看做是語法糖吧。比如:
view sourceprint?public class MyHashMap<TKey, TValue> {
private HashMap<TKey, TValue> m_map = new HashMap<TKey, TValue>();
public TValue get(TKey key) {
return this.m_map.get(key);
}
public void put(TKey key, TValue value) {
this.m_map.put(key, value);
}
public static void main(String[] args) {
MyHashMap<String, Integer> map = new MyHashMap<String, Integer>();
map.put("Hello", 5);
int i = map.get("Hello");
}
}
編譯成字節碼後,就成了下面這個樣子(這裡還用JAVA代碼來表示)
public class MyHashMap { private HashMap m_map = new HashMap(); public Object get(Object key) { return this.m_map.get(key); } public void put(Object key, Object value) { this.m_map.put(key, value); } public static void main(String[] args) { MyHashMap map = new MyHashMap(); map.put("Hello", 5); int i = (Integer)map.get("Hello"); } }好吧,看到Object,我承認我又想起裝箱了,可以看出Java中的泛型沒有解決裝箱問題。
由於JVM並不知道泛型類型,所以JAVA中就是以JAVA編譯器的語法糖的形式來表現的。當初我剛接觸JAVA的時候,的確會被下面幾種錯誤弄得很困惑。
view sourceprint?public class MyClass<SomeType> {
public static void myMethod(Object item) {
if (item instanceof SomeType) { // 報錯
...
}
SomeType st = new SomeType(); // 報錯
SomeType[] myArray = new SomeType[10]; // 報錯
}
}
在這裡我們可以想一下,到底怎麼樣才算真正的支持泛型呢?在.NET中,最終是由CLR根據元數據來執行IL代碼,因此,可以很容易理解:
1.IL中一定會有一個新指令來識別“類型參數”。
2.我們知道類型和方法的定義在元數據表中都會有相應的表示,因此為了支持泛型,元數據的格式也會有所改動。
3.修改JIT編譯器來執行新的IL指令。
也就是說,泛型類型定義能夠完整的編譯為MSIL類型。
泛型類型的運行大概的流程如下:
C#編譯器生成IL和元數據,表示泛型類定義,JIT編譯器則會把泛型類型定義與一系列的類型參數組合起來。
具體點來說,IL為初始化某個泛型類型的實例預留了占位符,JIT編譯器會在運行的時候,生成機器代碼的時候“補全定義”。JIT把相應的IL代碼編譯成X86指令,同時優化。優化什麼內容了呢?比如,在類型參數是引用類型的時候,就能使用相同的機器代碼來表示。為啥是引用類型而不是值類型呢?因為引用類型基本上都是指針,本質上來講結構都是一樣的。
這裡又要談一下類加載。JIT不是在某個類加載時就為其生成完整的X86指令,而是僅在類中的每個方法被第一次調用的時候才開始編譯的。(我現在覺得應該先講講類型,對象,線程棧和托管堆在運行時的相互關系比較好)。這樣,就會先在IL代碼上執行一個占位符替換步驟,替換成具體類型,隨後再像普通類一樣按需編譯。
好吧,你可以看出,在執行之前占位符被替換成具體類型了,因此泛型的匹配度是相當高的。應該說就是精確匹配。這個會影響什麼地方呢?在方法重載的時候就會有體現了。對於一個派生於MyBase的對象來說,WriteMEsaage<T>(T obj)要比WriteMEsaage(MyBase obj)在重載匹配上更優先。因為通過將T替換成MyDerived編譯器就可以完成一次“精確匹配”,而WriteMEsaage(MyBase obj)則還需要一次隱式轉換。於是泛型方法更有優勢,除非在調用時進行顯式類型轉換。下面用代碼說明:
view sourceprint?public class MyBase
{
}
public class MyDerived : MyBase
{
#region IMessageWriter Members
void IMessageWriter.WriteMessage()
{
Console.WriteLine("Inside MyDerived.WriteMessage");
}
#endregion
}
class Program
{
static void WriteMessage(MyBase b) {
Console.WriteLine("Inside WriteMessage(MyBase)");
}
static void WriteMessage<T>(T obj)
{
Console.Write("Inside WriteMessage<T>(T): ");
Console.WriteLine(obj.ToString());
}
static void Main(string[] args)
{
MyDerived d = new MyDerived();
Console.WriteLine("Calling Program.WriteMessage");
WriteMessage(d); //讓編譯器推斷使用哪個匹配方法
Console.WriteLine();
Console.WriteLine("Cast to base object");
WriteMessage((MyBase)d);
Console.WriteLine();
}
}
因此當你想支持某一類及其所有派生類時,基於基類創建泛型並不是最好的選擇。同樣的,基於接口也是如此。
那麼我想針對,這時就需要通過運行時來判斷了,當然,這並不是最好的解決方案,雖然對調用者屏蔽了具體的實現,但同時會帶運行時檢查的開銷。
view sourceprint?Static void WriteMessage<T>(T obj){
If(obje is MyBase){
WriteMessage(obj as MyBase); //顯式類型轉換
}else {
Conslole.Write(“Invoke WriteMessage<T>”)
}
}