比較C♯和Java

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

## 基本

### 對象處理

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

#### 引用

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

### 數據類型

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 來初始化內存空間的默認構造函數。
程式設計師只能定義另外的帶有一個或多個參數的構造函數。

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

#### 數組

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

#### 內部類

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

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

#### 部分類

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

#### 泛型

泛型編程

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

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

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

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

Java 不允許用基本數據類型來聲明為泛型類，然而 C# 卻允許不管是引用類型還是值類型被聲明為泛型，包括基本數據類型。Java 卻允許被封裝的類型作為泛型類的類型參數來使用 (例如：用 List<Integer> 代替 List<int>)，但是由於所有這一類的值需要在堆上分配而需付出一定的「代價」。
在 Java 和 C# 兩者中，泛型的定義都使用了不同的引用類型來分享等效的底層代碼，但是對 C# 來說公共語言運行時（CLR）為值類型的實例化動態的生成優化代碼。

### 符號和特殊功能

#### 特殊功能關鍵字

#### 回調和事件處理

### 數值應用

多種語言特色的存在是為了充分的支持應用程式在數學和金融領域計算。在這一類中，Java 提供關鍵字 strictfp 可以在代碼段中使浮點運算嚴格執行。這可以保證運算在所有的平台上都返回相同精確的結果。
與此不同 C# 為確保十進位小數浮點運算準確，在 `decimal` 類型中內嵌了這種機制。但在二進位小數浮點運算中捨棄了這種機制（`float`, `double`）。
在二進位所有的類型中描述十進位數因為不精確會存在捨入誤差。所以在金融應用方面十進位小數類型的精確顯得很重要。
Java 中 `BigDecimal` 類也提供了這些特性。任意精度小數算法 (`BigDecimal`) 和任意精度整數算法 (`BigInteger` ) 的類為其提供任意精度的數值運算。
儘管有第三方實現了這些類，但是.NET 框架 (3.5) 的現行版本當前並沒有提供這些。（參見 Arbitrary-precision arithmetic）
Java 不能為庫定義類型（高精度小數、複數等原始類型）提供一個統一標準，為了達到這個目的，C# 提供了如下內容：

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

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

#### 運算符重載

相比 Java，C# 包含了許多可數的便利。其中，例如運算符重載、用戶自定義類型，許多都被大批的 C++ 程式設計師所熟悉。
它還具有「外在的成員實現」，這樣可以讓一個類明確的實現一個接口中的方法，與自己類中的方法分離。或者為分別來自兩個接口中，具有相同函數名和簽名的函數提供不同的實現。
C# 包含了「索引器」，它可以當作是一種特殊的運算符（像 C++ 中的 `operator[]`），或者是用 `get`/`set` 訪問器來訪問類屬性。一個索引器用 `this[]` 來標明，
並且需要至少一個索引參數，該參數可以為任意類別：

Java 沒有提供運算符重載是為了阻止特徵濫用，還有為了語言的簡單。
C# 允許運算符重載（以確定的幾個限制來確保邏輯上的一致為條件），如果小心地使用，可以使代碼更加簡潔和易讀。

 

### 方法

在 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 關鍵字，它能有助於確保基類中具有相同簽名的方法仍然存在，並且能被正確的重寫。

#### 顯式接口實現

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

#### 開包

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

#### Lambdas 和表達樹

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

#### 部分方法

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

 

#### 擴展方法

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

#### 發生器方法

發生器方法是一個 C# 方法 ，這個方法被聲明為返回 `IEnumerable`,`IEnumerator` 接口或者這些接口的一般版本，該方法可以用 `yield` 語法實現。它是一個無限的表現形式，編譯器生成的補遺集，可大大減少所需的代碼遍歷或生成序列；雖然代碼只是通過編譯器生成。這個特徵過去也經常被用作實現無窮大的序列，就像斐波那契數列。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<int> 的形式，引入特殊類庫。

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) {}. 另一種對於檢查異常的評論說一個新方法的執行可能會引起意想不到的檢查異常被拋出，這是一個合同突破性變化。這可能發生在一個方法實現一個接口或者當一個方法的基本實現改變時，此接口僅聲明有限的異常。為這種意料之外的的異常被拋出，一些程式設計師簡單的聲明這種方法能拋出任何類型的異常（「拋出異常」），這使檢查異常的目的無法實現。不過在某些情況下，異常鏈（exception chaining）能用於代替，捕獲異常後再拋出一個異常異常。例如，如果一個對象訪問資料庫而不是文件時被改變，那麼可以捕獲 `SQLException` 異常並且作為 `IOException` 異常重新拋出。因為調用者也許並不需要知道對象內部的工作方式。

在處理 `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 () 方法總是被調用。

#### Finally 塊和未捕捉的異常

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

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

## 參考文獻

## 外部連結

* Contrasting C# and Java Syntax （頁面存檔備份，存於網際網路檔案館）
* Java vs. C# - Code for Code Comparison （頁面存檔備份，存於網際網路檔案館）
* Nine Language Performance Round-up （頁面存檔備份，存於網際網路檔案館）
* Java and C-Sharp Compared
* MSDN: The C# Programming Language for Java Developers （頁面存檔備份，存於網際網路檔案館）
* Standard ECMA-334 C# Language specification （頁面存檔備份，存於網際網路檔案館）
* Java Language Specification (Sun) （頁面存檔備份，存於網際網路檔案館）
* 31 Differences between C# and Java