1. 程式人生 > >為什麼JVM上沒有C#語言?淺談Type Erasure特性

為什麼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,再……