為什麼JVM上沒有C#語言?淺談Type Erasure特性
為什麼JVM上沒有C#語言?淺談Type Erasure特性
2010-02-22 23:50 by Jeffrey Zhao, 14796 閱讀, 107 評論, 收藏, 編輯每次提到語言的時候我總是忍不住罵Java是一門生產力低下,固步自封的語言——這估計要一直等到Java語言被JVM上的其他語言取代之後吧。JVM上目前已經有許多語言了:JRuby,Jython;還有一些特定於JVM平臺的語言,如Scala和Groovy等等。但是,為什麼JVM上沒有C#語言呢?按理說,這門和Java十分相似,卻又強大許多的語言更容易被Java程式設計師接受才對。您可能會說,Sun和微軟是對頭,怎麼可能將C#移植到JVM平臺上呢?嗯,有道理,但是為什麼社群裡也沒有人這麼做呢(要知道JVM上其他語言都是由社群發起的)?其實在我看來,這還是受到了技術方面的限制。
泛型是Java和C#語言的重要特性,它使得程式設計師可以方便地進行型別安全的程式設計,而不需要像以前那樣不斷進行型別轉換。例如,我們要在Java中寫一個泛型字典的封裝便可以這麼做:
public class DictWrapper { private HashMap<K, V> m_container = new HashMap<K, V>(); public V get(K key) { return this.m_container.get(key); } public void put(K key, V value) { this.m_container.put(key, value); } }
看上去和C#並沒有什麼區別,不是嗎?不過,如果我們觀察編譯後生成的bytecode(類似於.NET平臺上的IL),便會發現一絲奇妙之處。使用javap -c DictWrapper得到的結果是:
Compiled from "DictWrapper.java" public class jeffz.practices.DictWrapper extends java.lang.Object{ public jeffz.practices.DictWrapper(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: aload_0 5: new #2; //class java/util/HashMap 8: dup 9: invokespecial #3; //Method java/util/HashMap."<init>":()V 12: putfield #4; //Field m_container:Ljava/util/HashMap; 15: return public java.lang.Object get(java.lang.Object); Code: 0: aload_0 1: getfield #4; //Field m_container:Ljava/util/HashMap; 4: aload_1 5: invokevirtual #5; //Method java/util/HashMap.get:(Ljava/lang/Object;)Ljava/lang/Object; 8: areturn public void put(java.lang.Object, java.lang.Object); Code: 0: aload_0 1: getfield #4; //Field m_container:Ljava/util/HashMap; 4: aload_1 5: aload_2 6: invokevirtual #6; //Method java/util/HashMap.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; 9: pop 10: return }
從bytecode中可以看出,其中並沒有包含任何與K,V有關的資訊。get/put方法的引數和返回值都是Object型別,甚至內建的HashMap也是如此。那麼呼叫DictWrapper的程式碼是如何做到“強型別”的呢?例如:
public static void main(String[] args) { DictWrapper<String, String> dict = new DictWrapper<String, String>(); dict.put("Hello", "World"); String world = dict.get("Hello"); }
它的bytecode便是:
public static void main(java.lang.String[]); Code: 0: new #2; //class jeffz/practices/DictWrapper 3: dup 4: invokespecial #3; //Method jeffz/practices/DictWrapper."<init>":()V 7: astore_1 8: aload_1 9: ldc #4; //String Hello 11: ldc #5; //String World 13: invokevirtual #6; //Method jeffz/practices/DictWrapper.put:(Ljava/lang/Object;Ljava/lang/Object;)V 16: aload_1 17: ldc #4; //String Hello 19: invokevirtual #7; //Method jeffz/practices/DictWrapper.get:(Ljava/lang/Object;)Ljava/lang/Object; 22: checkcast #8; //class java/lang/String 25: astore_2 26: return }
看到標號為22的那行程式碼沒有?這條checkcast指令便是將上一句invokevirtual的結果轉化為String型別——DictWrapper.get所返回的是個最普通不過的Object。
這便是Java語言的泛型實現——請注意我這裡說的是Java語言,而不是JVM。因為JVM本身並沒有“泛型”的概念,Java語言的泛型則完全是編譯器的魔法。我們寫出的泛型程式碼,事實上都是和Object物件在打交道,是編譯器在幫我們省去了冗餘的型別轉換程式碼,以此保證了程式碼層面的型別安全。由於在執行時去除所有泛型的型別資訊,因此這種泛型實現方式叫做Type Erasure(型別擦除)。
在.NET中則完全不同,“泛型”是真真切切落實在CLR層面上的功能。例如DictWrapper.Get方法在.NET上的IL程式碼便是:
.method public hidebysig instance !TValue Get(!TKey key) cil managed { .maxstack 2 .locals init ( [0] !TValue CS$1$0000) L_0000: nop L_0001: ldarg.0 L_0002: ldfld class [mscorlib]...Dictionary`2 ...DictWrapper`2::m_container L_0007: ldarg.1 L_0008: callvirt instance !1 [mscorlib]...Dictionary`2::get_Item(!0) L_000d: stloc.0 L_000e: br.s L_0010 L_0010: ldloc.0 L_0011: ret }
您可以發現,.NET的IL便確切包含了TKey和TValue的型別資訊。而在執行的時候,CLR會為不同的泛型型別生成不同的具體型別程式碼,這在我之前的文章中也有所提及。
那麼,Java和C#兩種泛型實現方式分別有什麼優勢和劣勢呢?Java這種Type Erasure做法,最大的優勢便在於其相容性:即便使用了泛型,但最後生成的二進位制檔案也可以執行在泛型出現之前的JVM上(甚至JDK中不需要新增額外的類庫)——因為這裡的泛型根本不涉及JVM的變化。而.NET中的泛型需要CLR方面的“新能力”,因此.NET 2.0的程式集是無法執行在CLR 1.0上的——當然.NET 1.0的程式集可以直接在CLR 2.0上執行。而CLR實現方式的優勢,便在於可以在執行期間體現出“模板化”的優勢。.NET程式設計師都知道,泛型可以節省值型別的裝箱和拆箱的開銷,即便是引用型別也可以避免額外的型別轉化,這些都能帶來效能上的提高。
因此,在.NET社群經常會把Java的這種實現方式稱之為“假泛型”,而同時也會有人反駁到:泛型本來就是語言上的概念,實現不同又有什麼關係,憑什麼說是“假”的呢?其實,由於失去了JVM的支援,一些.NET平臺上常用的,非常有效的開發方式都難以運用在Java上。例如所謂的泛型字典:
public class Cache<TKey, TValue> { public static TValue Instance; } public class Factory { public static string Create<TKey>() { if (Cache<TKey, string>.Instance == null) { Cache<TKey, string>.Instance = // some expensive computation } return Cache<TKey, string>.Instance; } }
由於Cache<TKey>在執行時是個具體獨立的型別,因此泛型字典是效能最高的儲存方式,比O(1)時間複雜度的雜湊表還要高出許多。如果說這也只是執行方面的優勢,那麼這段程式碼中的“泛型工廠”程式碼(即Factory.Create<SomeType>(),包括類似的Factory<T>.Create()這種)則是Java語言中無法實現的。這是因為Type Erasure的作用,在執行時JVM已經喪失了TKey這樣的型別資訊,而在.NET平臺上,TKey則是Create<TKey>簽名的組成部分。
Type Erasure造成的限制還有不少,如果您是一個C#程式設計師,可能難以相信以下的Java程式碼都是不合法的:
public class MyClass<E> { public static void myMethod(Object item) { if (item instanceof E) { // Compiler error ... } E item2 = new E(); // Compiler error E[] iArray = new E[10]; // Compiler error } }
由於JVM不提供對泛型的支援,因此對於JVM上支援泛型的語言,如Scala,這方面的壓力就完全落在編譯器身上了。而且,由於這些語言以JVM為底,Type Erasure會影響JVM平臺上幾乎所有語言。以Scala為例,它的模式匹配語法可以用來判斷一個變數的型別:
value match { case x:String => println("Value is a String") case x:HashMap[String, Int] => println("Value is HashMap[String, Int]") case _ => println("Value is not a String or HashMap[String, Int]") }
猜猜看,如果value變數是個HashMap[Int, Object]型別的物件,上面的程式碼會輸出什麼結果呢?如果是C#或是F#這樣執行在.NET平臺上的語言,最終輸出的一定是“Value is not ...”。只可惜,由於JVM的Type Erasure特性,以上程式碼輸出的卻是“Value is HashMap[String, Int]”。這是因為在執行期間JVM並不包含泛型的型別資訊,HashMap[K, V]即是HashMap,無論HashMap[String, Int]還是HashMap[Int, Object]都是HashMap,JVM無法判斷不同泛型型別的集合之間有什麼區別。不過還好,Scala編譯器遇到這種情況會發出警告,程式設計師可以瞭解這些程式碼可能會出現的“誤會”。
因此,為什麼有IKVM.NET這樣的專案可以將Java語言編譯成.NET程式集(也可以將Java的jar包轉化成.NET程式集),卻沒有專案將C#編譯到JVM上(或是將C#程式集轉化為jar包)。這是因為,JVM不足以支撐C#語言所需要的所有特性。而從執行時的中間程式碼角度來說,JVM Bytecode的能力也是.NET IL的子集——又有什麼辦法可以將超集塞入它的子集呢?
此外,如CLR的值型別可能也很難直接落實在JVM上,這也是JVM上執行C#的又一阻礙。由於這些因素存在,我想如F#這樣的.NET語言也幾乎不可能出現在JVM上了。
當然,如果真要在JVM上實現完整的C#也並非不可以。只要在JVM上進行一層封裝(例如還是就叫做CLR,CLR Language Runtime),一定可以滿足C#的全部要求。但是這個代價太高,即使實現了這點可能也沒什麼實際意義。而事實上,已經有人在JVM上實現了一個x86模擬器,那麼又有什麼是做不了的呢?實在不行,我們就在模擬器上裝一個Windows作業系統,然後裝一個Microsoft .NET,再……