比較 C♯和 Java

本文對比了 C# 與 Java 這兩種程式語言，這兩種語言都具有自動垃圾回收以及即時編譯的特點，並且兩者的語法都有主要繼承自 C 語言 / C++，因此二者有很多相似之處。但由於 C# 是作為 C++ 和 Java 的混合體而在 Java 之後所創造的，因此 C# 相較 Java 有更多的新語法特徵並且引入了一些增強的功能，所以固然兩者有其相似性，但是總歸還是有諸多不同之處。

基本

1.1 物件處理

C# 和 Java 都可以被視作一種類似使用動態分派的類 C++ 的面嚮物件語言（其中 C++ 又源自於 C 語言）。但需要注意，這 C# 和 Java 並非 C 或者 C++ 的超集，它們之間是沒有直接衍生附屬關係的獨立語言。C# 和 Java 都使用垃圾回收作為一種回收記憶體資源的手段，而不是直接的釋放記憶體。C# 和 Java 都包含執行緒同步機制作為他們語法的一部分。

1.1.1 引用

C# 允許指標的有限功能的使用，指標和運算指標在一個操作的環境中是存在潛在的不安全性的，因為他們的使用可以避開物件的一些嚴格訪問規則。C# 中使用指標的程式碼段或者方法的地址要用 unsafe 關鍵字進行標記，這樣，這些程式碼的使用者就會知道這個程式碼相比其他的程式碼而言是不具有安全性的。編譯器需要 unsafe 關鍵字時將使用此程式碼的程式轉換成是允許被編譯的。一般來說，不安全程式碼的使用可能是為了非託管的 API（應用程式程式設計介面）的更好互用，或者是為了（存在內在不安全性的）系統呼叫，也有可能是出於提高效能等方面的原因。而 Java 中不允許指標或者算術指標的使用。

1.2 資料型別

java 和 C# 語言都有原始資料型別的概念，C#/.NET 語言中支援的原始資料（所有的，除了 string 型別）都是值型別。但 C# 比 java 支援更多的原始資料型別，比如整型和十進位制浮點數，尤其是 java 缺少無符號的 BYTE 型別，而 C# 的 BYTE 型別預設是無符號的。在兩種語言中 string 其值都是不可改變的一個類，但是特殊的是 C# 為其提供了特殊的構造方法，同時 C# 還可以像值型別一樣的使用 string 的值就而不需要進行拆箱操作。

既允許自動裝箱和拆箱，把它們從物件型別轉換為原始資料。實際上，這使得原始型別成為物件型別的子型別。在 C＃中這也意味著，原始型別可以定義方法，如覆蓋的物件的 ToString 的方法。在 Java 中，單獨的原始包裝類提供這種功能。在 Java 中原始值不含隱式裝箱和一個顯示的型別轉換都需要一個例項稱為原始值的 ((Integer) 42).toString 而不是 C# 中呼叫例項 42.ToString 。另一個不同之處在於，java 使大量使用裝箱型別 (見下文)，這樣可以讓一個隱式拆箱轉換 (在 C ＃這需要一個型別轉換)。由於這些隱性拆箱轉換可能會丟擲空指標例外，現代整合開發環境和編譯器可以配置為突出它們。

值型別

C# 允許程式設計師用關鍵字 struct 建立使用者自定義的值型別 (value type)。

從程式設計師的角度來講，它們可以被看做輕量級的類。

不同於一般類，而像標準基本類，這種值型別被分配在棧記憶體 (stack) 而不是堆記憶體 (heap)。

結構體通常有一系列的限制，因為結構體沒有空值的概念並且可以在陣列中無需初始化而直接使用，這種型別也有必須用 0 來初始化記憶體空間的預設建構函式。

程式設計師只能定義另外的帶有一個或多個引數的建構函式。

這也意味著結構體缺少一個虛方法表，正因為這樣（還有固定的記憶體空間），它們不允許繼承（但可以實現介面）。

1.2.1 陣列

陣列和集合類同樣在語法中給出了重要意義，感謝基於迭代器的預宣告迴圈。在 C# 裡一個數組反映為一個數組類的物件，而在 JAVA 每個陣列都是一個直接的物件集的子集（但是可以對映為一個以它真正的成員類為父類的一個數組），並且不實現任何的集合介面。C# 擁有真正的多維陣列，如同 Java 中可用到的陣列的陣列（在 C# 中通常稱為鋸齒陣列）。多維陣列可以因為增強位置（就像有一個單一的指示器解除參照，代替陣列的每一維作為鋸齒陣列的容器）在某些情況下增強效能。另一個優點是整個多維陣列可以用單一的 new 運算子申請而賦值，而鋸齒陣列需要對每一維進行迴圈和賦值。注意，儘管 Java 為分配多維的鋸齒陣列提供依據句法的整齊的陣列長度（在 C# 術語中是一個矩形陣列），迴圈和多樣的分配被虛擬機器完成不需要外在的來源。

1.2.2 內部類

java 與 C# 都允許設定內部類，即在一個類內部定義的另一個類。在 java 中，這些內部類可以訪問外部類的靜態和非靜態成員（除非這個內部類定義為靜態的，在這種情況下只能訪問外部類的靜態成員）。區域性內部類可以定義在一個方法中並訪問這個方法中宣告為 final 型別的區域性變數，匿名區域性類允許構造類的例項用來重寫類的方法。

C# 也提供內部類，與 Java 不同的是它需要外部類的非靜態成員的一個明確引用。同時 C# 提供匿名類作為一個結構用來訪問區域性變數和方法（參見事件處理）。區域性類和匿名類不能被訪問。

1.2.3 部分類

C# 使用部分類允許一個類的定義分割在幾個原始檔中。每一個部分必須用關鍵字 partial 標記。作為一個單一的彙編的部分所有的部分都必須提交給編譯器。每個部分可以引用其它部分的成員。每個部分都可以實現介面，並且某個部分可以定義一個基類。這個功能在程式碼生成時非常有用，也就是一個程式碼發生器提供一部分程式碼，開發商提供另一部分程式碼，兩種程式碼在一起編譯。因此開發商可以編輯他們的部分程式碼而不用冒著程式碼發生器在以後覆蓋這部分程式碼的危險。和類擴充套件機制不同，部分類在它的部分之間允許迴圈依賴，因為它們在編譯的時候都保證被解決。Java 沒有類似的概念。

1.2.4 泛型

泛型程式設計

現在的程式語言都支援泛型程式設計，但它們卻採用了不同的實現方式。

Java 中的泛型僅是語言層面上的一種結構，它們只能透過編譯器來實現。生成的類檔案中所包含的類簽名僅由元資料組成（允許編譯器對這些新類進行反編譯）。執行時並不知道通用型別系統，這意味著 JVM 只需要進行一小部分的更新便可處理新的類格式。

為了實現這個目標，編譯器用泛型型別的上界來替換它們，並且在用到這些泛型的各個地方適當地插入一些 “角色”。結果生成的位元組碼將不包含任何對這些泛型型別的引用或將它們作為引數。這種實現泛型的技術被稱作型別擦除。這意味著實際上的型別的資訊在執行時不可用，並且強行加入了一些限制，例如不能建立泛型的新例項或陣列。(參見 Java 中的泛型)。

C# 採用了另一種實現方式。它對泛型的支援是整合在虛擬執行系統中的，並且最早出現在.NET2.0 中。這門語言後來就發展為在執行系統中支援基本泛型的前端。而在 Java 中，編譯器提供了靜態型別安全檢查，但是，加之又有即時編譯器（JIT）載入來核實其正確性。關於泛型型別的資訊在執行時完全被保護起來了，並且允許完全的反射和例項化泛型型別。

Java 不允許用基本資料型別來宣告為泛型類，然而 C# 卻允許不管是引用型別還是值型別被宣告為泛型，包括基本資料型別。Java 卻允許被封裝的型別作為泛型類的型別引數來使用 (例如：用 List 代替 List)，但是由於所有這一類的值需要在堆上分配而需付出一定的 “代價”。

在 Java 和 C# 兩者中，泛型的定義都使用了不同的引用型別來分享等效的底層程式碼，但是對 C# 來說公共語言執行時（CLR）為值型別的例項化動態的生成最佳化程式碼。

1.3 符號和特殊功能

1.3.1 特殊功能關鍵字

1.3.2 回撥和事件處理

1.4 數值應用

多種語言特色的存在是為了充分的支援應用程式在數學和金融領域計算。在這一類中，Java 提供關鍵字 strictfp 可以在程式碼段中使浮點運算嚴格執行。這可以保證運算在所有的平臺上都返回相同精確的結果。

與此不同 C# 為確保十進位制小數浮點運算準確，在 decimal 型別中內嵌了這種機制。但在二進位制小數浮點運算中捨棄了這種機制（float, double）。

在二進位制所有的型別中描述十進位制數因為不精確會存在舍入誤差。所以在金融應用方面十進位制小數型別的精確顯得很重要。

Java 中類也提供了這些特性。任意精度小數演算法 (BigDecimal) 和任意精度整數演算法 ( ) 的類為其提供任意精度的數值運算。

儘管有第三方實現了這些類，但是.NET 框架 (3.5) 的現行版本當前並沒有提供這些。（參見 Arbitrary-precision arithmetic）

Java 不能為庫定義型別（高精度小數、複數等原始型別）提供一個統一標準，為了達到這個目的，C# 提供瞭如下內容：

能夠提供方便語法的運算子過載和索引（看下面）。
隱性和顯性轉換；允許諸如嵌入式 int 型別隱性轉換為 long 型別的存在。
值型別和基於值型別的屬性；在 Java 中每個常規型別必須被存放在堆疊中，它對常規型別和儲存型別的效能是不利的。

除此之外，C# 能用 checked 和 unchecked 運算子幫助數學計算，當在一段程式碼中出現算數溢位時它能夠檢測出是否能夠繼續執行。它也提供在內嵌陣列的某些應用方面有優勢的矩陣。

1.4.1 運算子過載

相比 Java，C# 包含了許多可數的便利。其中，例如運算子過載、使用者自定義型別，許多都被大批的 C++ 程式設計師所熟悉。

它還具有 “外在的成員實現”，這樣可以讓一個類明確的實現一個介面中的方法，與自己類中的方法分離。或者為分別來自兩個介面中，具有相同函式名和簽名的函式提供不同的實現。

C# 包含了 “索引器”，它可以當作是一種特殊的運算子（像 C++ 中的 operator），或者是用

get/set 訪問器來訪問類屬性。一個索引器用 this 來標明，

並且需要至少一個索引引數，該引數可以為任意類別：

myList4 = 5;

string name = xmlNode.Attributes"name";

orders = customerMaptheCustomer;

Java 沒有提供運算子過載是為了阻止特徵濫用，還有為了語言的簡單。

C# 允許運算子過載（以確定的幾個限制來確保邏輯上的一致為條件），如果小心地使用，可以使程式碼更加簡潔和易讀。

1.5 方法

在 C# 中，方法在預設狀態下是非虛擬的，如果希望得到一個虛方法則必須明確地用 virtual 修飾符進行宣告，而在 Java 當中，所有非靜態、非私有的方法都是虛方法。虛方法保證被呼叫的總是該方法最近被重寫的那個實現。但是，由於各個過載方法之間不能被正常地進行內聯，而使得在方法呼叫上需要花費一個相當長的執行時間，並且需要透過虛方法列表進行間接的呼叫。然而，包括 Sun 公司所推薦的實現方法在內的一些 Java 虛擬機器的實現方法，則會對最普遍被呼叫的那些虛方法執行內聯。在 java 中，方法在預設狀態下是虛擬的。（儘管他們能透過使用 “final” 修飾符來密封以使他不允許被覆蓋）。沒有什麼辦法讓 subclass 或 derived class 以同樣的名字定義一個新的、無關聯的方法。 這就會產生一個問題，即當一個基類由一個不同的人定義，這時就有可能出現一個與派生類中已經定義過的一些方法有著相同的名字和標籤的新的版本的方法定義。在 Java 中，這種情況將意味著派生類中的同名方法會隱式的重寫基類中的方法，儘管這種結果不是所有設計者的真正意圖。為了防止這種版本問題，C# 中要求將派生類中需要重寫虛方法的部分進行顯示的宣告。 如果一個方法需要被重寫，那麼必須指定 override 修飾符。如果不希望進行方法重寫，而且類的設計者僅僅希望引出一個新的方法以影射舊的方法，那麼就必須含有 new 關鍵字。New 關鍵字和 override 關鍵字也避免了由於基類中的 protecte 方法或 public 方法在它的某一個派生類中被使用時所帶來的問題。Java 中重新編譯將導致編譯器把派生類中的這種方法當做是基類方法的重寫，而這可能並不是基類的開發者想要的。

而 C＃編譯器將會把這種方法預設為 new 關鍵字已經被指定，但仍會對這種結果發出警告。為了部分地容納這些版本問題， Java 5.0 中引入了 @override 註釋，但為了保護它的向後相容這種做法不會被當作是強制性的，所以它並不能阻止上述意外的重寫情況。然而對於 C＃中的 override 關鍵字，它能有助於確保基類中具有相同簽名的方法仍然存在，並且能被正確的重寫。

1.5.1 顯式介面實現

如果在多個介面中有一個方法（或 C ＃中的屬性）具有相同名稱和簽名，當一個類在實現這些介面時這些重名的成員就會產生衝突。一個解決方法是透過為所有介面實現一個預設共同的方法。如果必須要分開來實現（因為這個方法確實要實現某個特殊的目的，或者是因為各個介面的返回值不一樣）。C# 顯示介面的實現將解決這一問題。在 java 中消除命名衝突的問題只能透過重構或者是定義更多的介面來避免。C# 的顯示介面實現還能隱藏底層基礎的類和介面，因此使得減少類和介面的複雜性。

1.5.2 開包

當一個函式作為一個引數來傳遞併為後面的程式呼叫，這時候會出現一個問題：當這個方法呼叫了它自己作用域內的變數時會怎樣呢？C# 中有真正的開包功能，方法的引用會完全的獲得它自己作用域範圍內的變數。Java 中，匿名內部類只能呼叫到作用域內的常方法，想要呼叫和更新內部類的話，就必須透過開發人員的手工宣告額外的間接的父類來實現。

1.5.3 Lambdas 和表達樹

C# 中的一個特殊型別稱 "lambdas"。 他們不是方法也不可能構成類介面的部分；他們只是在功能模組中。 在 lambda 函式頂部可以定義的一個詳細結構體稱為表達樹。 不管他們是被當成執行函式還是資料結構都起決於編輯器型別，並且不管什麼型別變數或參量都要賦值。 Lambdas 和表示樹在 LINQ 中都是重要角色。 Java 中沒有以 lambdas 或表達樹為特色的；它的主要機制和方法定義是匿名內部類句法。

1.5.4 部分方法

與 "部分類" 相關 C# 允許部分方法在部分類之內指定。 一個部分方法是方法的一個故意宣告並且在簽名上有一定的約束。 這些約束指定，如果任何類成員沒有被定義，那麼可以安全地刪除。 這個特點允許程式碼提供大量的監聽點 (像 GoF 設計模式中的 "模板方法") 而不用花費多餘時間，如果另一個類成員在編譯時沒有引用它們。而 Java 沒有對應的概念。

1.5.5 擴充套件方法

用一個特殊的 this 指定在一個方法的第一個引數 C# 允許這個方法扮演成第一個引數型別的一個成員函式。這個外來類的 “擴充套件” 是完全句法的。這個擴充套件方法需要變為靜態的，而且定義在一個完全的靜態類中。它必須服從在外部靜態方法上的任何限定，因此它不能摧毀物件封裝。這個 “擴充套件” 僅僅是在靜態宿主類的名稱空間被引進的範圍內是活躍的。在 java 裡面，相同的效果可以透過一個另一個類的一般方法得到，但語法將是一個函式呼叫，而不是方法呼叫類的 C# 語法擴充套件。

1.5.6 發生器方法

發生器方法是一個 C# 方法 ，這個方法被宣告為返回 IEnumerable,IEnumerator 介面或者這些介面的一般版本，該方法可以用 yield 語法實現。它是一個無限的表現形式，編譯器生成的補遺集，可大大減少所需的程式碼遍歷或生成序列；雖然程式碼只是透過編譯器生成。這個特徵過去也經常被用作實現無窮大的序列，就像斐波那契數列。java 是沒有相應的概念。

1.6 條件編譯

與 Java 不同，C# 使用預編譯指令實現了條件編譯的功能。它還提供了條件屬性，使方法只有在定義了編譯常量的時候才被執行。這樣一來，只有在定義了 DEBUG 常量時，Debug.Assert 方法才會執行，斷言成為了 framework 的特色。從 1.4 版本開始，Java 開始提供斷言，預設情況下在執行時被關閉，但也可以在呼叫 JVM 時使用 "-enableassertions" 或者 "-ea" 開啟。

1.7 名字空間和原始檔

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。

1.8 異常處理

Java 支援檢查異常 (checked exception)。C＃中只支援非檢查異常情況。檢查異常強制程式設計師要麼在方法中宣告一個異常丟擲，要麼用 try-catch 來捕獲異常。檢查異常可以有助於良好的程式設計習慣，以確保所有的錯誤都得到處理。但是 Anders Hejlsberg，C# 語言首席設計師，和其他人爭辯說，他們都在一定程度上對 Java 進行了拓展但是它們沒有被證明是有價值的除了幾個程式中的小例子。有一個評論介紹在檢查異常時鼓勵程式設計師使用空的 catch 塊，安靜的吃掉異常而不是讓異常傳播到更高水平的常規的異常處理：catch (Exception e) . 另一種對於檢查異常的評論說一個新方法的執行可能會引起意想不到的檢查異常被丟擲，這是一個合同突破性變化。這可能發生在一個方法實現一個介面或者當一個方法的基本實現改變時，此介面僅宣告有限的異常。為這種意料之外的的異常被丟擲，一些程式設計師簡單的宣告這種方法能丟擲任何型別的異常（“丟擲異常”），這使檢查異常的目的無法實現。不過在某些情況下，異常鏈（exception chaining）能用於代替，捕獲異常後再丟擲一個異常異常。例如，如果一個物件訪問資料庫而不是檔案時被改變，那麼可以捕獲 異常並且作為異常重新丟擲。因為呼叫者也許並不需要知道物件內部的工作方式。

在處理 try-finally 的宣告時兩種語言也是有差別的。即使 try 塊包含像 throw 和 return 的 control-passing 語句，finally 塊也總是要執行。在 Java 中，這可能導致意外的行為，如果 try 塊最後有 return 語句返回一個值，然後執行後的 finally 塊也會有 return 語句返回一個不同的值。 C# 利用禁止任何像 return 或者 break 的 control-passing 語句來解決這一問題。

使用 try-finally 塊的普遍原因是為了保護管理程式碼的資源，所以珍貴的資源被保證在 finally 塊中釋出。作為句法速記為共同的設想的 using 語句在 C# 中處於顯著地位，其中 using 的物件的 Dispose 方法總是被呼叫。

1.8.1 Finally 塊和未捕捉的異常

(C# 派生的異常特點) 對 CLI（公共語言基礎）的 ECMA（歐洲電腦廠商協會）標準指出在堆疊的兩次搜尋中處理異常。 ECMA-355 4th Edition 12.4.2.5 Overview of exception handling 首次透過嘗試找到一個匹配的 catch 塊，如果沒有找到就終止該程式。只有當找到匹配的 catch 塊時，才會在第二步執行，從而執行干預的 finally 塊。這使得問題在程式狀態還沒第一次被 finally 塊修改前被診斷；它也消除了當程式在未知狀態下，finally 塊可能有副作用的風險（例如，外部資料的損壞或進一步引發的異常）。

Java 語言規範中指出 finally 塊中的程式碼總會執行即使異常沒有被捕獲，並且舉出例項程式碼演示期待的結果。

1.9 底層的程式碼

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 ＃環境中繼續工作以完成某些任務，否則將需要呼叫非託管程式碼。使用不安全程式碼的程式或程式集必須透過進行特殊的轉換才能被編譯並且將依此被標記。這使得執行時環境在潛在地執行有危險的程式碼前要採取特別的預防措施。