比較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)為值類型的實例化動態的生成優化代碼。

符號和特殊功能[編輯]

特殊功能關鍵字[編輯]

關鍵字 功能,實例
checked, unchecked 在C#里, checked 聲明塊或表達式可以在運行時檢查算術的溢出。
get, set C#實現屬性作為語言語法的一部分,而且選用相應的getset 訪問器, 而Java的訪問方法, 不是一種語言功能,而是基於方法命名公約的編碼方式。
goto C#中支持goto關鍵字。goto有時候是有用的, 舉個例子,實現有限的狀態機或者生成的代碼, 但是通常建議使用更加合理控制流程的結構化方法(見goto語句的評論)。 Java 允許使用breaks和continues彌補了goto語句的的許多用途。
switch(color)
{
    case Color.Blue:
         Console.WriteLine("Color is blue"); break;
    case Color.DarkBlue:
         Console.WriteLine("Color is dark");
         goto case Color.Blue;
    // ...
}
out, ref C#支持輸出參數和引用參數。這使得c#可以從一個方法返回多個值或者通過引用傳遞多個值。
strictfp Java 使用關鍵字 strictfp 確保跨平台時浮點運算的結果保持不變。
switch 在C#里, switch 語句也操作於string型和long型,但是只允許失敗的空白語句。 Java switch 語句在Java7之後才支援操作strings;不能操作於long 的原始類型 但是能通過所有的空白語句(不包括那些含有 'break'的語句)。
throws Java中要求每個方法都要聲明它能拋出檢測異常或者檢測異常的父類。任何方法也可以隨意的定義它所拋出的非檢測異常,C#中卻沒有這樣的語法規則。
public int readItem() throws java.io.IOException
{
    // ...
}
using C#中的using指令使得對象的Dispose方法(通過IDisposable接口被執行)定義為在代碼塊執行之後或者在代碼塊之中的異常被拋出時才被執行。
//创建一个小文件"test.txt",写一个字符串,
//... 并且把它关闭(即使发生了异常)
using (StreamWriter file = new StreamWriter("test.txt"))
{
    file.Write("test");
}
yield C#語言中允許使用yield關鍵字來表示迭代器。在Java中,迭代器只能用類(可以是匿名的)來定義,且需要很多的樣板代碼。下面是一個能夠讀取可迭代的輸入(可以是數組)並且返回所有偶數成員的迭代器的例子。
public static IEnumerable<int> GetEven(IEnumerable<int> numbers)
{
    foreach (int i in numbers)
    {
        if (i % 2 == 0)
            yield return i;
    }
}

回調和事件處理[編輯]

數值應用[編輯]

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

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

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

運算符重載[編輯]

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

myList[4] = 5;
string name = xmlNode.Attributes["name"];
orders = customerMap[theCustomer];

Java沒有提供運算符重載是為了阻止特徵濫用,還有為了語言的簡單。[2] 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塊包含像throwreturn的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塊中的代碼總會執行即使異常沒有被捕獲,並且舉出實例代碼演示期待的結果。[3]

底層的代碼[編輯]

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

參考文獻[編輯]

  1. ^ 1.0 1.1 Java for Scientific Computation: Prospects and Problems (PDF). [2009-05-01]. (原始內容 (PDF)存檔於2007-09-22). 
  2. ^ August 1998 Java News. [2009-05-01]. (原始內容存檔於2009-01-25). 
  3. ^ Java Language Spec. 3rd Edition 14.20.2 Execution of try-catch-finally. [2009-05-01]. (原始內容存檔於2009-03-10). 

外部連結[編輯]