比較C♯和Java

本文對比了C#與Java這兩種程式語言，這兩種語言都具有自動垃圾回收以及執行時編譯執行的特點，並且兩者的語法都有主要繼承自C語言/C++，因此二者有很多相似之處。但由於C#是作為C++和Java的混合體而在Java之後所創造的，因此C#相較Java有更多的新語法特徵並且引入了一些增強的功能，所以固然兩者有其相似性，但是總歸還是有諸多不同之處。基本.物件處理.C#和Java都可以被視作一種類似使用動態分派的類C++的面嚮物件語言。但需要注意，這C#和Java並非C或者C++的超集，它們之間是沒有直接衍生附屬關係的獨立語言。C#和Java都使用垃圾回收作為一種回收記憶體資源的手段，而不是直接的釋放記憶體。C#和Java都包含執行緒同步機制作為他們語法的一部分。引用.C#允許指標的有限功能的使用，指標和運算指標在一個操作的環境中是存在潛在的不安全性的，因為他們的使用可以避開物件的一些嚴格訪問規則。C#中使用指標的程式碼段或者方法的地址要用unsafe關鍵字進行標記，這樣，這些程式碼的使用者就會知道這個程式碼相比其他的程式碼而言是不具有安全性的。編譯器需要unsafe關鍵字時將使用此程式碼的程式轉換成是允許被編譯的。一般來說，不安全程式碼的使用可能是為了非託管的API系統呼叫，也有可能是出於提高效能等方面的原因。而Java中不允許指標或者算術指標的使用。資料型別.java和C#語言都有原始資料型別的概念，C#/.NET語言中支援的原始資料都是值型別。但C#比java支援更多的原始資料型別，比如整型和十進位制浮點數，尤其是java缺少無符號的BYTE型別，而C#的BYTE型別預設是無符號的。在兩種語言中string其值都是不可改變的一個類，但是特殊的是C#為其提供了特殊的構造方法，同時C#還可以像值型別一樣的使用string的值就而不需要進行拆箱操作。既允許自動裝箱和拆箱，把它們從物件型別轉換為原始資料。實際上，這使得原始型別成為物件型別的子型別。在C＃中這也意味著，原始型別可以定義方法，如覆蓋的物件的ToString()的方法。在Java中，單獨的原始包裝類提供這種功能。在Java中原始值不含隱式裝箱和一個顯示的型別轉換都需要一個例項稱為原始值的((Integer)42).toString()而不是C#中呼叫例項 42.ToString()。另一個不同之處在於，java使大量使用裝箱型別(見下文)，這樣可以讓一個隱式拆箱轉換(在C ＃這需要一個型別轉換)。由於這些隱性拆箱轉換可能會丟擲空指標例外,現代整合開發環境和編譯器可以配置為突出它們。值型別C#允許程式設計師用關鍵字codice_1建立使用者自定義的值型別(value type)。從程式設計師的角度來講，它們可以被看做輕量級的類。不同於一般類，而像標準基本類，這種值型別被分配在棧記憶體(stack)而不是堆記憶體(heap)。結構體通常有一系列的限制，因為結構體沒有空值的概念並且可以在陣列中無需初始化而直接使用，這種型別也有必須用0來初始化記憶體空間的預設建構函式。程式設計師只能定義另外的帶有一個或多個引數的建構函式。這也意味著結構體缺少一個虛方法表，正因為這樣。陣列.陣列和集合類同樣在語法中給出了重要意義，感謝基於迭代器的預宣告迴圈。在C#裡一個數組反映為一個數組類的物件，而在JAVA每個陣列都是一個直接的物件集的子集，迴圈和多樣的分配被虛擬機器完成不需要外在的內部類.java與C#都允許設定內部類，即在一個類內部定義的另一個類。在java中，這些內部類可以訪問外部類的靜態和非靜態成員。區域性內部類可以定義在一個方法中並訪問這個方法中宣告為final型別的區域性變數，匿名區域性類允許構造類的例項用來重寫類的方法。C#也提供內部類，與Java不同的是它需要外部類的非靜態成員的一個明確引用。同時C#提供匿名類作為一個結構用來訪問區域性變數和方法。區域性類和匿名類不能被訪問。部分類.C#使用"部分類"允許一個類的定義分割在幾個原始檔中。每一個部分必須用關鍵字codice_2標記。作為一個單一的彙編的部分所有的部分都必須提交給編譯器。每個部分可以引用其它部分的成員。每個部分都可以實現介面，並且某個部分可以定義一個基類。這個功能在程式碼生成時非常有用，也就是一個程式碼發生器提供一部分程式碼，開發商提供另一部分程式碼，兩種程式碼在一起編譯。因此開發商可以編輯他們的部分程式碼而不用冒著程式碼發生器在以後覆蓋這部分程式碼的危險。和類擴充套件機制不同，部分類在它的部分之間允許迴圈依賴，因為它們在編譯的時候都保證被解決。Java沒有類似的概念。泛型.泛型程式設計現在的程式語言都支援泛型程式設計，但它們卻採用了不同的實現方式。Java中的泛型僅是語言層面上的一種結構，它們只能透過編譯器來實現。生成的類檔案中所包含的類簽名僅由元資料組成。執行時並不知道通用型別系統，這意味著JVM只需要進行一小部分的更新便可處理新的類格式。為了實現這個目標，編譯器用泛型型別的上界來替換它們，並且在用到這些泛型的各個地方適當地插入一些“角色”。結果生成的位元組碼將不包含任何對這些泛型型別的引用或將它們作為引數。這種實現泛型的技術被稱作型別擦除。這意味著實際上的型別的資訊在執行時不可用，並且強行加入了一些限制，例如不能建立泛型的新例項或陣列。(ava中的泛型)。C#採用了另一種實現方式。它對泛型的支援是整合在虛擬執行系統中的，並且最早出現在.NET2.0中。這門語言後來就發展為在執行系統中支援基本泛型的前端。而在Java中，編譯器提供了靜態型別安全檢查，但是，加之又有即時編譯器載入來核實其正確性。關於泛型型別的資訊在執行時完全被保護起來了，並且允許完全的反射和例項化泛型型別。Java不允許用基本資料型別來宣告為泛型類，然而C#卻允許不管是引用型別還是值型別被宣告為泛型，包括基本資料型別。Java卻允許被封裝的型別作為泛型類的型別引數來使用(例如：用List)，但是由於所有這一類的值需要在堆上分配而需付出一定的“代價”。在Java和C#兩者中，泛型的定義都使用了不同的引用型別來分享等效的底層程式碼，但是對C#來說公共語言執行時為值型別的例項化動態的生成最佳化程式碼。數值應用.多種語言特色的存在是為了充分的支援應用程式在數學和金融領域計算。在這一類中，Java提供關鍵字strictfp可以在程式碼段中使浮點運算嚴格執行。這可以保證運算在所有的平臺上都返回相同精確的結果。與此不同C#為確保十進位制小數浮點運算準確，在 codice_3型別中內嵌了這種機制。但在二進位制小數浮點運算中捨棄了這種機制。在二進位制所有的型別中描述十進位制數因為不精確會存在舍入誤差。所以在金融應用方面十進位制小數型別的精確顯得很重要。Java中類也提供了這些特性。任意精度小數演算法 (codice_6) 和任意精度整數演算法 ( ) 的類為其提供任意精度的數值運算。儘管有第三方實現了這些類，但是.NET框架(3.5)的現行版本當前並沒有提供這些。Java不能為庫定義型別提供一個統一標準，為了達到這個目的，C#提供瞭如下內容：除此之外，C#能用checked和unchecked運算子幫助數學計算，當在一段程式碼中出現算數溢位時它能夠檢測出是否能夠繼續執行。它也提供在內嵌陣列的某些應用方面有優勢的矩陣。運算子過載.相比Java，C#包含了許多可數的便利。其中，例如運算子過載、使用者自定義型別，許多都被大批的C++程式設計師所熟悉。它還具有“外在的成員實現”，這樣可以讓一個類明確的實現一個介面中的方法，與自己類中的方法分離。或者為分別來自兩個介面中，具有相同函式名和簽名的函式提供不同的實現。C#包含了“索引器”，它可以當作是一種特殊的運算子，或者是用codice_8/codice_9 訪問器來訪問類屬性。一個索引器用codice_10來標明，並且需要至少一個索引引數，該引數可以為任意類別：myList[4] = 5;string name = xmlNode.Attributes["name"];orders = customerMap[theCustomer];Java沒有提供運算子過載是為了阻止特徵濫用，還有為了語言的簡單。C#允許運算子過載，如果小心地使用，可以使程式碼更加簡潔和易讀。方法.在C#中，方法在預設狀態下是非虛擬的，如果希望得到一個虛方法則必須明確地用 virtual 修飾符進行宣告，而在Java當中，所有非靜態、非私有的方法都是虛方法。虛方法保證被呼叫的總是該方法最近被重寫的那個實現。但是，由於各個過載方法之間不能被正常地進行內聯，而使得在方法呼叫上需要花費一個相當長的執行時間，並且需要透過虛方法列表進行間接的呼叫。然而，包括Sun公司所推薦的實現方法在內的一些Java虛擬機器的實現方法，則會對最普遍被呼叫的那些虛方法執行內聯。在java中，方法在預設狀態下是虛擬的。。沒有什麼辦法讓subclass或derived class以同樣的名字定義一個新的、無關聯的方法。 這就會產生一個問題，即當一個基類由一個不同的人定義，這時就有可能出現一個與派生類中已經定義過的一些方法有著相同的名字和標籤的新的版本的方法定義。在Java中，這種情況將意味著派生類中的同名方法會隱式的重寫基類中的方法，儘管這種結果不是所有設計者的真正意圖。為了防止這種版本問題，C#中要求將派生類中需要重寫虛方法的部分進行顯示的宣告。 如果一個方法需要被重寫，那麼必須指定override修飾符。如果不希望進行方法重寫，而且類的設計者僅僅希望引出一個新的方法以影射舊的方法，那麼就必須含有new關鍵字。New關鍵字和override關鍵字也避免了由於基類中的protecte方法或public方法在它的某一個派生類中被使用時所帶來的問題。Java中重新編譯將導致編譯器把派生類中的這種方法當做是基類方法的重寫，而這可能並不是基類的開發者想要的。而C＃編譯器將會把這種方法預設為new關鍵字已經被指定，但仍會對這種結果發出警告。為了部分地容納這些版本問題， Java 5.0中引入了@override註釋，但為了保護它的向後相容這種做法不會被當作是強制性的，所以它並不能阻止上述意外的重寫情況。然而對於C＃中的override關鍵字，它能有助於確保基類中具有相同簽名的方法仍然存在，並且能被正確的重寫。顯式介面實現.如果在多個介面中有一個方法。C#顯示介面的實現將解決這一問題。在java中消除命名衝突的問題只能透過重構或者是定義更多的介面來避免。C#的顯示介面實現還能隱藏底層基礎的類和介面，因此使得減少類和介面的複雜性。開包.當一個函式作為一個引數來傳遞併為後面的程式呼叫，這時候會出現一個問題：當這個方法呼叫了它自己作用域內的變數時會怎樣呢？C#中有真正的開包功能，方法的引用會完全的獲得它自己作用域範圍內的變數。Java中，匿名內部類只能呼叫到作用域內的常方法，想要呼叫和更新內部類的話，就必須透過開發人員的手工宣告額外的間接的父類來實現。Lambdas和表達樹.C#中的一個特殊型別稱" lambdas"。 他們不是方法也不可能構成類介面的部分; 他們只是在功能模組中。 在lambda函式頂部可以定義的一個詳細結構體稱為表達樹。 不管他們是被當成執行函式還是資料結構都起決於編輯器型別，並且不管什麼型別變數或參量都要賦值。 Lambdas和表示樹在LINQ中都是重要角色。 Java中沒有以lambdas或表達樹為特色的; 它的主要機制和方法定義是匿名內部類句法。部分方法.與"部分類"相關 C#允許部分方法在部分類之內指定。 一個部分方法是方法的一個故意宣告並且在簽名上有一定的約束。 這些約束指定，如果任何類成員沒有被定義，那麼可以安全地刪除。 這個特點允許程式碼提供大量的監聽點(像GoF設計模式中的"模板方法")而不用花費多餘時間，如果另一個類成員在編譯時沒有引用它們。而 Java沒有對應的概念。擴充套件方法.用一個特殊的"this"指定在一個方法的第一個引數C#允許這個方法扮演成第一個引數型別的一個成員函式。這個外來類的“擴充套件”是完全句法的。這個擴充套件方法需要變為靜態的，而且定義在一個完全的靜態類中。它必須服從在外部靜態方法上的任何限定，因此它不能摧毀物件封裝。這個“擴充套件”僅僅是在靜態宿主類的名稱空間被引進的範圍內是活躍的。在java裡面，相同的效果可以透過一個另一個類的一般方法得到，但語法將是一個函式呼叫，而不是方法呼叫類的C#語法擴充套件。發生器方法.發生器方法是一個C#方法 ，這個方法被宣告為返回codice_11,codice_12介面或者這些介面的一般版本，該方法可以用 codice_13語法實現。它是一個無限的表現形式, 編譯器生成的補遺集，可大大減少所需的程式碼遍歷或生成序列；雖然程式碼只是透過編譯器生成。這個特徵過去也經常被用作實現無窮大的序列，就像斐波那契數列。java是沒有相應的概念。條件編譯.與Java不同，C#使用預編譯指令實現了條件編譯的功能。它還提供了條件屬性，使方法只有在定義了編譯常量的時候才被執行。這樣一來，只有在定義了DEBUG常量時，Debug.Assert()方法才會執行，斷言成為了framework的特色。從1.4版本開始，Java開始提供斷言，預設情況下在執行時被關閉，但也可以在呼叫JVM時使用 "-enableassertions" 或者 "-ea" 開啟。名字空間和原始檔.C#的名稱空間和C++類似，但不同於Java的包機制，C#名稱空間不會以任何方式依賴於原始檔的位置，這與Java不同，Java的常規結構要求原始檔的位置必須和包目錄結構相符。這兩種語言都允許引入類庫( 例:import java.util.* ,Java方式)，在引入類庫後，使用類時就可以直接透過類名引用。不同名字空間或包中可以具有相同名字的類，這樣類在使用時可以透過全限定名來引用，或者透過不同的名字只引入必要的類。基於這個問題，Java允許引入單個類(例:import java.util.List)。C#允許在引入類庫時 使用語句: using Console = System.Console來為一個類庫定義一個新名，它同樣允許以using IntList = System.Collections.Generic.List的形式，引入特殊類庫。Java有允許使用某些或所有，具有較短名字的靜態方法/領域的靜態import句法在類中(例如，允許foo(bar)可以從另一個類中被靜態的引進).C#有靜態類句法(不與Java的靜態內在類混淆),制約類只包含靜態方法。 C# 3.0介紹的引申方法允許使用者靜態地增加方法到型別(比如，允許foo.bar 的地方可以是研究foo的種類的一個引進的引申方法)。Sun Microsystems 軟體公司的Java編譯器要求，原始檔的檔名必須匹配在它裡面的唯一的公開類，而C#允許在同一個檔案的多公開類，並且投入制約。 C# 2.0和以後的版本允許類定義被分割成幾個檔案，透過使用在原始程式碼的關鍵字partial。異常處理.Java支援檢查異常(checked exception)。C＃中只支援非檢查異常情況。檢查異常強制程式設計師要麼在方法中宣告一個異常丟擲，要麼用try-catch來捕獲異常。檢查異常可以有助於良好的程式設計習慣，以確保所有的錯誤都得到處理。但是Anders Hejlsberg，C#語言首席設計師，和其他人爭辯說，他們都在一定程度上對Java進行了拓展但是它們沒有被證明是有價值的除了幾個程式中的小例子。有一個評論介紹在檢查異常時鼓勵程式設計師使用空的catch塊,安靜的吃掉異常而不是讓異常傳播到更高水平的常規的異常處理：catch (Exception e) {}.另一種對於檢查異常的評論說一個新方法的執行可能會引起意想不到的檢查異常被丟擲，這是一個合同突破性變化。這可能發生在一個方法實現一個介面或者當一個方法的基本實現改變時，此介面僅宣告有限的異常。為這種意料之外的的異常被丟擲,一些程式設計師簡單的宣告這種方法能丟擲任何型別的異常能用於代替,捕獲異常後再丟擲一個異常異常.例如,如果一個物件訪問資料庫而不是檔案時被改變,那麼可以捕獲 異常並且作為異常重新丟擲. 因為呼叫者也許並不需要知道物件內部的工作方式。在處理codice_14的宣告時兩種語言也是有差別的。即使codice_15塊包含像codice_16和codice_17的control-passing語句，finally塊也總是要執行。在Java中，這可能導致意外的行為，如果try塊最後有codice_17語句返回一個值，然後執行後的codice_19塊也會有codice_17語句返回一個不同的值。 C#利用禁止任何像return或者break的control-passing語句來解決這一問題。使用try-finally 塊的普遍原因是為了保護管理程式碼的資源，所以珍貴的資源被保證在finally 塊中釋出。作為句法速記為共同的設想的using語句在C#中處於顯著地位,其中using的物件的Dispose()方法總是被呼叫。Finally塊和未捕捉的異常.(C# 派生的異常特點)對CLI。Java語言規範中指出finally塊中的程式碼總會執行即使異常沒有被捕獲，並且舉出例項程式碼演示期待的結果。底層的程式碼.Java Native Interface (JNI)的特徵是允許Java程式碼呼叫非Java程式碼。然而，JNI要求被呼叫的程式碼必須遵循Java提供的一些在型別和名稱上的約定。這種方法是為了適應Java和其他程式碼更好的互動。這些程式碼必須是非Java程式碼，常常是C或者C++程式碼。JNA提供一種更加方便的Java程式碼與其他程式碼的互動，僅僅需要寫一些Java編寫的介面程式碼，但是效能會付出一點代價。另外，第三方類庫為JAVA－COM提供橋接，像JACOB (自由軟體)，J-Integra for COM  (專有軟體).NET平臺呼叫(P/Invoke)透過允許從C#呼叫微軟稱之為不受託管程式碼提供同樣的的功能，透過元資料屬性程式設計師可以精確的控制如何呼叫引數和結果，因此可以避免額外編譯程式碼的需要。平臺呼叫允許幾乎完全的對程式的API的訪問(像Win32或POSIX)但是限制對c++類庫的訪問。另外，.NET框架也提供一個.NET－COM網橋，允許對COM元件的的訪問就像是訪問本地的.NET元件。C＃中還允許程式設計師禁用正常型別檢查和CLR中其他的安全保證功能 ，這樣就使得指標變數的使用成為可能。當此功能被使用時，程式設計師必須用unsafe關鍵字將相應的程式碼段進行標記。JNI ，P/Invoke，和“unsafe”的程式碼段是相當冒險的部分，它揭露了可能的安全漏洞和應用不穩定。使用unsafe的一個優勢是，透過P/Invoke或JNI運行於託管執行環境中的程式碼是讓程式設計師在比較熟悉的C ＃環境中繼續工作以完成某些任務，否則將需要呼叫非託管程式碼。使用不安全程式碼的程式或程式集必須透過進行特殊的轉換才能被編譯並且將依此被標記。這使得執行時環境在潛在地執行有危險的程式碼前要採取特別的預防措施。