原文地址:http://www.25hoursaday.com/CsharpVsJava.html
簡介
C#語言是一門面向對象的語言,開發者可以使用C#和微軟.NET平台快速構建各種應用程序。C#和.NET平台的目標是把開發者從一些諸如內存管理、類型安全問題、底層類庫、數組邊界檢查等等的底層問題中解放出來並節約大量的時間,這樣開發者就可以真正把時間和精力放在他們的應用程序和業務邏輯上。對於Java開發者來說,把前面那句話的開頭改為“Java語言和平台”,這句話也同樣可以總結Java語言和平台。
后面的內容介紹了C#和Java編程語言的異同,這些都是基於我使用兩個語言的經歷。所有代碼都經過微軟.NET框架2.0以及Java SE 6的測試。
注意:作者有些代碼不符合.NET 3.5或JAVA SE 7(或以上)版本的最佳實踐寫法並且也不能覆蓋它們所提供的新語法和新特性,但不影響本文的重點也就是平台的比較。
第一部分:C#和JAVA基本一致的地方
1、我們都是Object
C#的類層次中有一個根,也就是所有C#的類都是System.Object的子類,Java也是這樣,所有類都是java.lang.Object的子類。兩個語言的Object類的方法有些相同(比如System.Object的ToString()和java.lang.Object的toString()),也有一些不同(System.Object沒有java.lang.Object中的wait()、notify()或notifyAll())。
注意:在C#中object類可以寫成object或Object。小寫的object C#關鍵字在編譯的時候會替換為System.Object。
2、關鍵字一覽
Java和C#有很多語法很相似,除了throws、transient和strictfp幾乎所有Java關鍵字都有C#的對應。下表示Java和C#的關鍵字對照表,Java關鍵字標紅而C#關鍵字標藍。
注意:盡管goto和const是Java語言的關鍵字,但是在Java中並沒有用到。C#中的[NonSerialized]特性等價於Java中的transient關鍵字。
3、虛擬機和語言運行時
Java一般編譯成Java字節碼並運行於托管的執行環境(Java虛擬機、JVM),同樣,C#代碼編譯成中間語言(IL)運行於公共語言運行時(CLR)。兩個平台都通過JIT編譯器提供本機編譯。
注意:雖然Java平台支持字節碼的解釋和JIT編譯兩種方式,但是.NET平台只支持C#代碼的本機執行,IL代碼在運行前總是會編譯成本機代碼。
4、基於堆和垃圾收集
在Java中,對象使用new關鍵字創建在堆上。在C#中大多數類使用new關鍵字創建在堆上。和JVM一樣,CLR也是通過標記和壓縮垃圾回收算法管理銷毀對象。
注意:C#還支持基於棧的類,叫做值類型,后文會介紹到這個。
5、數組可以是交錯的
對於C或C++這樣的語言,多維數組的每一個子數組都必須有相同的維度。在Java和C#中數組不必統一,交錯數組可以認為是數組的一維數組。交錯數組的項就是保持類型或引用的另一個數組,這樣交錯數組的行和列就不需要有統一的長度,如下代碼所示:
int [][]myArray = new int[2][]; myArray[0] = new int[3]; myArray[1] = new int[9];
6、沒有全局方法
和Java一樣,和C++不一樣,C#中的方法必須是類的一部分,作為成員方法或靜態方法。
7、有接口但沒有多重繼承
C#和Java一樣支持接口的概念,接口類似純抽象類。C#和Java一樣都支持類的單繼承,但支持借口的多重繼承(或實現)。
8、字符串不可變
C#的System.String類和java.lang.String類相似。它們都是不可變的,也就是字符串的值在創建后一次都不能修改。字符串提供的一些實例方法看似可以修改字符串的內容,其實是創建了一個新的字符串並返回,原始的字符串並沒有修改。如下的C#和Java代碼都沒有修改字符串:
C# Code String csString = "Apple Jack"; csString.ToLower(); Java Code String jString = "Grapes"; jString.toLowerCase();
9、密封類
Java和C#都提供了一種機制確保類是繼承體系中的最后一個,不可以有子類。在Java中可以為類修飾final關鍵字,而C#則通過sealed關鍵字修飾類。如下是兩種語言密封類的例子:
C# Code sealed class Student { string fname; string lname; int uid; void attendClass() {} } Java Code final class Student { String fname; String lname; int uid; void attendClass() {} }
10、拋出和捕獲異常
C#和Java的異常有很多相似的地方。兩種語言都使用try塊來表示需要守護的區域,catch塊來處理拋出的異常,finally塊在離開方法之前釋放資源。兩種語言都有繼承體系,所有的異常都從一個Exception類繼承。並且都可以在捕獲到異常並進行了一些錯誤處理之后重新拋出異常。最后,它們都提供了機制把異常包裝成另外一個異常,這樣就可以捕獲到一個異常后拋出另一個異常。這里可以舉一個例子,比如三層結構的應用程序,數據訪問的時候捕獲到了一個SQLException,可以拋出一個應用程序相關的異常。這樣的話,應用程序異常可以使用原始的SQLException來初始化,處理應用程序異常的時候如果需要還可以訪問到原始的異常。如下代碼演示了兩種語言在異常方面的相似:
C# Code using System; using System.IO; class MyException: Exception{ public MyException(string message): base(message){ } public MyException(string message, Exception innerException): base(message, innerException){ } } public class ExceptionTest { static void DoStuff(){ throw new FileNotFoundException(); } public static void Main(string[] args){ try{ try{ DoStuff(); return; //won't get to execute }catch(IOException ioe){ /* parent of FileNotFoundException */ throw new MyException("MyException occured", ioe); /* rethrow new exception with inner exception specified */ } }finally{ Console.WriteLine("***Finally block executes even though MyException not caught***"); } }//Main(string[]) } // ExceptionTest Java Code class MyException extends Exception{ public MyException(String message){ super(message); } public MyException(String message, Exception innerException){ super(message, innerException); } } public class ExceptionTest { static void doStuff(){ throw new ArithmeticException(); } public static void main(String[] args) throws Exception{ try{ try{ doStuff(); return; //won't get to execute }catch(RuntimeException re){ /* parent of ArithmeticException */ throw new MyException("MyException occured", re); /* rethrow new exception with cause specified */ } }finally{ System.out.println("***Finally block executes even though MyException not caught***"); } }//main(string[]) } // ExceptionTest
11、定義的時候進行成員初始化和靜態構造方法
C#和Java中都可以在定義實例和靜態變量的時候進行初始化。如果成員變量是實例變量則在構造方法執行之前調用,靜態成員則在第一次使用成員以及第一次創建類實例之前初始化。還可以指定一段代碼在類創建之前或靜態方法調用之前被調用。這段代碼在C中叫做靜態構造方法而在Java中叫做靜態初始化塊。靜態構造方法會在第一次調用類的靜態方法或第一次創建類實例之前調用。
using System; class StaticInitTest{ string instMember = InitInstance(); static string staMember = InitStatic(); StaticInitTest(){ Console.WriteLine("In instance constructor"); } static StaticInitTest(){ Console.WriteLine("In static constructor"); } static String InitInstance(){ Console.WriteLine("Initializing instance variable"); return "instance"; } static String InitStatic(){ Console.WriteLine("Initializing static variable"); return "static"; } static void DoStuff(){ Console.WriteLine("Invoking static DoStuff() method"); } public static void Main(string[] args){ Console.WriteLine("Beginning main()"); StaticInitTest.DoStuff(); StaticInitTest sti = new StaticInitTest(); Console.WriteLine("Completed main()"); } }
class Main{ String instMember = initInstance(); static String staMember = initStatic(); Main(){ System.out.println("In instance constructor"); } static{ System.out.println("In static constructor"); } static String initInstance(){ System.out.println("Initializing instance variable"); return "instance"; } static String initStatic(){ System.out.println("Initializing static variable"); return "static"; } static void doStuff(){ System.out.println("Invoking static DoStuff() method"); } public static void main(String[] args){ System.out.println("Beginning main()"); Main.doStuff(); Main sti = new Main(); System.out.println("Completed main()"); } }
代碼執行結果:
Initializing static variable
In static constructor
Beginning main()
Invoking static DoStuff() method
Initializing instance variable
In instance constructor
Completed main()
注意:這里作者的代碼有誤,小小修改了一下
12、裝箱
在某些情況下,值類型需要當做對象,.NET和Java運行時會自動把值類型轉換成在堆上分配的引用類型,這個過程叫做裝箱。自動把對象轉換成相應的值類型的過程叫做拆箱,比如把java.lang.Integer的實例轉換成int。如下例子演示了運行時發生裝箱的各種情況:
C# Code using System; using System.Collections; //分配在棧上的結構需要裝箱才能當做object struct Point{ //member fields private int x; private int y; public Point (int x, int y){ this.x = x; this.y = y; } public override string ToString(){ return String.Format("({0}, {1})", x, y); } }//Point class Test{ public static void PrintString(object o){ Console.WriteLine(o); } public static void Main(string[] args){ Point p = new Point(10, 15); ArrayList list = new ArrayList(); int z = 100; PrintString(p); //p在傳參的時候裝箱了 PrintString(z); //z在傳參的時候裝箱了 // 在保存到集合的時候int和float裝箱了 // 不需要Java的包裝類 list.Add(1); list.Add(13.12); list.Add(z); for(int i =0; i < list.Count; i++) PrintString(list[i]); } }
Java Code import java.util.*; class Test{ public static void PrintString(Object o){ System.out.println(o); } public static void PrintInt(int i){ System.out.println(i); } public static void main(String[] args){ Vector list = new Vector(); int z = 100; Integer x = new Integer(300); PrintString(z); //z boxed to object when passed to PrintString PrintInt(x); //x unboxed to int when passed to PrintInt // integers and float boxed when stored in collection // therefore no need for Java wrapper classes list.add(1); list.add(13.12); list.add(z); for(int i =0; i < list.size(); i++) PrintString(list.elementAt(i)); } }
第二部分:C#和JAVA基本一致但語法不同的地方
1、Main方法
C#和Java程序的入口點都是main方法。表面區別是C#的Main方法是大寫的M(.NET框架的方法名的慣例),而Java中的main方法是小寫字母m(同樣也是Java方法的慣例)。還有一個區別就是C#的Main()方法可以沒有參數。
C# Code using System; class A{ public static void Main(String[] args){ Console.WriteLine("Hello World"); } } Java Code class B{ public static void main(String[] args){ System.out.println("Hello World"); } }
一般推薦應用程序的每一個類都有一個main方法來測試類的功能,比如可能有兩個類A和B都包含main方法。在Java中,類是編譯的單元,只需要通過命令行指定需要運行的類,在C#中也可以使用/main開關編譯應用程序指定應用程序創建時使用哪個main作為入口點實現相同的效果。使用main以及預處理指令條件編譯是不錯的測試技術。
Java Example C:\CodeSample> javac A.java B.java C:\CodeSample> java A Hello World from class A C:\CodeSample> java B Hello World from class B C# Example C:\CodeSample> csc /main:A /out:example.exe A.cs B.cs C:\CodeSample> example.exe Hello World from class A C:\CodeSample> csc /main:B /out:example.exe A.cs B.cs C:\CodeSample> example.exe Hello World from class B
對於Java,改變使用的main不需要進行重新編譯,而C#應用程序需要重新編譯。但是,從另一方面來說,Java又不支持條件編譯,main方法可能就會帶入發行版本中。
2、繼承語法
C#對於繼承使用了C++的語法,都使用冒號來實現繼承和接口實現,在Java中則是extends和implements關鍵字。
C# Code using System; class B:A, IComparable{ int CompareTo(){} public static void Main(String[] args){ Console.WriteLine("Hello World"); } } Java Code class B extends A implements Comparable{ int compareTo(){} public static void main(String[] args){ System.out.println("Hello World"); } }
C#的這種語法更符合從C++轉來開發者的習慣,而Java的語法則可以通過類的聲明直接知道類是子類呢還是只是實現了接口,C#則無法區分。但是我們知道根據.NET的命名約定,接口需要以大寫的字母I打頭,比如ICloneable,這樣就不會有這個問題了。
3、運行時類型標識(is操作符)
C#的js操作符和Java的instanceof操作符一樣。如下代碼等價:
C# Code if(x is MyClass) MyClass mc = (MyClass) x; Java Code if(x instanceof MyClass) MyClass mc = (MyClass) x;
4、命名空間
C#命名空間是對類進行分組的方式,和Java的包構造差不多。使用C++的人會發現C#命名空間和C++的差不多,在Java中,包名字代筆應用程序中源文件目錄結構,而C#的命名空間則不會要求源文件的物理層次和邏輯結構有關聯:
C# Code namespace com.carnage4life{ public class MyClass { int x; void doStuff(){} } } Java Code package com.carnage4life; public class MyClass { int x; void doStuff(){} }
C#的命名空間語法允許進行命名空間的嵌套,如下:
C# Code using System; namespace Company{ public class MyClass { /* Company.MyClass */ int x; void doStuff(){} } namespace Carnage4life{ public class MyOtherClass { /* Company.Carnage4life.MyOtherClass */ int y; void doOtherStuff(){} public static void Main(string[] args){ Console.WriteLine("Hey, I can nest namespaces"); } }// class MyOtherClass }// namespace Carnage4life }// namespace Company
5、構造方法、析構方法以及終結器
C#中的構造方法的語法和語義和Java一樣。C#還有析構方法的概念,這和C++的析構器語法比較相似,和Java的終結器語義一致。盡管存在析構方法這樣的語法,但是還是不推薦使用,有許多原因。首先我們沒有辦法控制它運行的時間,如果在析構方法里面還持有其它引用的話情況更復雜,其次還會帶來性能問題,因為垃圾回收線程不能直接回收帶有析構方法的對象,必須等到終結器線程執行之后才可以回收,這樣具有析構方法的對象可能比沒有析構方法的對象存在的時間更長。如下是C#和Java的例子:
C# Code using System; public class MyClass { static int num_created = 0; int i = 0; MyClass(){ i = ++num_created; Console.WriteLine("Created object #" + i); } ~MyClass(){ Console.WriteLine("Object #" + i + " is being finalized"); } public static void Main(string[] args){ for(int i=0; i < 10000; i++) new MyClass(); } } Java Code public class MyClass { static int num_created = 0; int i = 0; MyClass(){ i = ++num_created; System.out.println("Created object #" + i); } public void finalize(){ System.out.println("Object #" + i + " is being finalized"); } public static void main(String[] args){ for(int i=0; i < 10000; i++) new MyClass(); } }
注意:在C#中析構方法(終結器)會在執行后自動調用基類的終結器,Java中不會。
6、同步方法和代碼塊
在Java中可以指定同步塊來確保在同一時刻只有一個線程訪問某個對象,C#提供了lock語句對應Java的synchronized語句。
C# Code public void WithdrawAmount(int num){ lock(this){ if(num < this.amount) this.amount -= num; } } Java Code public void withdrawAmount(int num){ synchronized(this){ if(num < this.amount) this.amount -= num; } }
C#和Java都有同步方法的概念。在調用同步方法的時候,調用方法的線程會鎖定包含方法的對象,因此其它線程調用相同對象的同步方法必須等到其它線程執行完方法釋放鎖之后才能執行。同步方法在Java中用synchronized關鍵字來標記,而在C#中使用[MethodImpl(MethodImplOptions.Synchronized)]特性來修飾。同步方法的例子如下:
C# Code using System; using System.Runtime.CompilerServices; public class BankAccount{ [MethodImpl(MethodImplOptions.Synchronized)] public void WithdrawAmount(int num){ if(num < this.amount) this.amount - num; } }//BankAccount Java Code public class BankAccount{ public synchronized void withdrawAmount(int num){ if(num < this.amount) this.amount - num; } }//BankAccount
7、訪問修飾符
如下表格對照C#和Java的訪問修飾符。C++愛好者比較失望,在Java2中Sun改變了protected關鍵字的語義,而C#的protected關鍵字和C++的一樣。也就是說,protected成員只有在類的成員方法或派生類的成員方法中可以訪問。internal修飾符表示成員可以被類相同程序集的其它類訪問。internal protected修飾符表示的是internal或protected。
注意:在沒有指定訪問修飾符的情況下,C#字段或方法的默認訪問級別是private,而Java則是protected。
8、反射
在C#和Java中發現類中方法和字段以及運行時調用類方法的能力一般叫做反射。Java和C#中反射的主要區別是,C#的反射是程序集級別的,而Java是類級別的。由於程序集一般保存為DLL,對於C#需要包含類的DLL,而Java需要可以加載類文件或目標類。如下例子遍歷某個類的方法,以此比較C#和Java在反射上的差異:
C# Code using System; using System.Xml; using System.Reflection; using System.IO; class ReflectionSample { public static void Main( string[] args){ Assembly assembly=null; Type type=null; XmlDocument doc=null; try{ // 加載程序集以獲得類型 assembly = Assembly.LoadFrom("C:\\WINNT\\Microsoft.NET\\Framework\\v1.0.2914\\System.XML.dll"); type = assembly.GetType("System.Xml.XmlDocument", true); //Unfortunately one cannot dynamically instantiate types via the Type object in C#. doc = Activator.CreateInstance("System.Xml","System.Xml.XmlDocument").Unwrap() as XmlDocument; if(doc != null) Console.WriteLine(doc.GetType() + " was created at runtime"); else Console.WriteLine("Could not dynamically create object at runtime"); }catch(FileNotFoundException){ Console.WriteLine("Could not load Assembly: system.xml.dll"); return; }catch(TypeLoadException){ Console.WriteLine("Could not load Type: System.Xml.XmlDocument from assembly: system.xml.dll"); return; }catch(MissingMethodException){ Console.WriteLine("Cannot find default constructor of " + type); }catch(MemberAccessException){ Console.WriteLine("Could not create new XmlDocument instance"); } // 獲得方法 MethodInfo[] methods = type.GetMethods(); //打印方法簽名和參數 for(int i=0; i < methods.Length; i++){ Console.WriteLine ("{0}", methods[i]); ParameterInfo[] parameters = methods[i].GetParameters(); for(int j=0; j < parameters.Length; j++){ Console.WriteLine (" Parameter: {0} {1}", parameters[j].ParameterType, parameters[j].Name); } }//for (int i...) } }
上面的代碼可以看到C#的反射API略微優雅一點,C#提供了ParameterInfo包含方法的參數元數據,而Java提供的只是Class對象丟失了諸如參數名等信息。
有的時候需要獲取指定類元數據對象,那么可以使用Java的java.lang.Class或C#的System.Type對象。要從類的實例獲取元數據,在Java中可以使用getClass()方法而在C#中可以使用GetType()方法。如果要根據名字而不是創建一個類的實例來獲取元數據可以這么做:
C# Code Type t = typeof(ArrayList); Java Code Class c = java.util.Arraylist.class; /* 必須在類的完整名字后跟 .class */
9、聲明常量
在Java中要聲明常量可以使用final關鍵字。final的變量可以在編譯時或運行時進行設置。在Java中,如果在基元上使用final的話基元的值不可變,如果在對象引用上使用final的話,則引用只可以指向一個對象。final的成員可以在聲明的時候不初始化,但是必須要構造方法中初始化。
在C#中要聲明常量使用const關鍵字來表示編譯時常量,使用readonly關鍵字來表示運行時常量。基元常量和引用常量的語義對於C#和Java來說是一樣的。
和C++不同的是,C#和Java不能通過語言結構來指定不可變的類。
C# Code using System; public class ConstantTest{ /* 編譯時常量*/ const int i1 = 10; //隱含表示這是static的變量 // 下面的代碼不能通過編譯 // public static const int i2 = 20; /* 運行時常量 */ public static readonly uint l1 = (uint) DateTime.Now.Ticks; /* 對象引用作為常量 */ readonly Object o = new Object(); /* 未初始化的只讀變量 */ readonly float f; ConstantTest() { // 未初始化的只讀變量必須在構造方法中初始化 f = 17.21f; } } Java Code import java.util.*; public class ConstantTest{ /* Compile time constants */ final int i1 = 10; //instance variable static final int i2 = 20; //class variable /* run time constants */ public static final long l1 = new Date().getTime(); /* object reference as constant */ final Vector v = new Vector(); /* uninitialized final */ final float f; ConstantTest() { // unitialized final variable must be initialized in constructor f = 17.21f; } }
注意:Java語言還支持方法上的final參數。在C#中沒有這個功能。final參數只要用於允許傳入方法的參數可以讓方法內的內部類進行訪問。
10、基元類型
對於Java中的每一個基元類型在C#中都有同名的對應(除了byte之外)。Java中的byte是有符號的,等價於C#中的sbyte,不同於C#的byte。C#還提供了一些基元類型的無符號版本,比如ulong、uint、ushort和byte。C#中最不同的基元類型是decimal類型,不會有舍入錯誤,當然也就需要更多空間也更慢。
C# Code decimal dec = 100.44m; //m is the suffix used to specify decimal numbers double dbl = 1.44e2d; //e is used to specify exponential notation while d is the suffix used for doubles
11、數組聲明
Java有兩種方式聲明數組。一種方式為了兼容C/C++的寫法,另外一種更具有可讀性,C#只能使用后者。
C# Code int[] iArray = new int[100]; //valid, iArray is an object of type int[] float fArray[] = new float[100]; //ERROR: Won't compile Java Code int[] iArray = new int[100]; //valid, iArray is an object of type int[] float fArray[] = new float[100]; //valid, but isn't clear that fArray is an object of type float[]
12、調用基類構造方法和構造方法鏈
C#和Java自動調用基類構造方法,並且提供了一種方式可以調用基類的有參構造方法。兩種語言都要求派生類的構造方法在任何初始化之前先調用基類的構造方法防止使用沒有初始化的成員。兩種語言還提供了在一個構造方法中調用另一個構造方法的方式以減少構造方法中代碼的重復。這種方式叫做構造方法鏈:
C# Code using System; class MyException: Exception { private int Id; public MyException(string message): this(message, null, 100){ } public MyException(string message, Exception innerException): this(message, innerException, 100){ } public MyException(string message, Exception innerException, int id): base(message, innerException){ this.Id = id; } } Java Code class MyException extends Exception{ private int Id; public MyException(String message){ this(message, null, 100); } public MyException(String message, Exception innerException){ this(message, innerException, 100); } public MyException( String message,Exception innerException, int id){ super(message, innerException); this.Id = id; } }
13、可變長度參數列表
在C和C++中可以指定函數接收一組參數。在printf和scanf類似的函數中大量使用這種特性。C#和Java允許我們定義一個參數接收可變數量的參數。在C#中可以在方法的最后一個參數上使用params關鍵字以及一個數組參數來實現,在Java中可以為類型名通過增加三個.來實現。
C# Code using System; class ParamsTest{ public static void PrintInts(string title, params int[] args){ Console.WriteLine(title + ":"); foreach(int num in args) Console.WriteLine(num); } public static void Main(string[] args){ PrintInts("First Ten Numbers in Fibonacci Sequence", 0, 1, 1, 2, 3, 5, 8, 13, 21, 34); } } Java Code class Test{ public static void PrintInts(String title, Integer... args){ System.out.println(title + ":"); for(int num : args) System.out.println(num); } public static void main(String[] args){ PrintInts("First Ten Numbers in Fibonacci Sequence", 0, 1, 1, 2, 3, 5, 8, 13, 21, 34); } }
14、泛型
C#和Java都提供了創建強類型數據結構且不需要在編譯時知道具體類型的機制。在泛型機制出現以前,這個特性通過在數據結構中指定object類型並且在運行時轉換成具體類型來實現。但是這種技術有許多缺點,包括缺乏類型安全、性能不佳以及代碼膨脹。
如下代碼演示了兩種方式:
# Code using System; using System.Collections; using System.Collections.Generic; class Test{ public static Stack GetStackB4Generics(){ Stack s = new Stack(); s.Push(2); s.Push(4); s.Push(5); return s; } public static Stack<int> GetStackAfterGenerics(){ Stack<int> s = new Stack<int>(); s.Push(12); s.Push(14); s.Push(50); return s; } public static void Main(String[] args){ Stack s1 = GetStackB4Generics(); int sum1 = 0; while(s1.Count != 0){ sum1 += (int) s1.Pop(); //cast } Console.WriteLine("Sum of stack 1 is " + sum1); Stack<int> s2 = GetStackAfterGenerics(); int sum2 = 0; while(s2.Count != 0){ sum2 += s2.Pop(); //no cast } Console.WriteLine("Sum of stack 2 is " + sum2); } }
Java Code import java.util.*; class Test{ public static Stack GetStackB4Generics(){ Stack s = new Stack(); s.push(2); s.push(4); s.push(5); return s; } public static Stack<Integer> GetStackAfterGenerics(){ Stack<Integer> s = new Stack<Integer>(); s.push(12); s.push(14); s.push(50); return s; } public static void main(String[] args){ Stack s1 = GetStackB4Generics(); int sum1 = 0; while(!s1.empty()){ sum1 += (Integer) s1.pop(); //cast } System.out.println("Sum of stack 1 is " + sum1); Stack<Integer> s2 = GetStackAfterGenerics(); int sum2 = 0; while(!s2.empty()){ sum2 += s2.pop(); //no cast } System.out.println("Sum of stack 2 is " + sum2); } }
盡管和C++中的模板概念相似,C#和Java中的泛型特性實現上不一樣。在Java中,泛型功能使用類型擦除來實現。泛型信息只是在編譯的時候出現,編譯后被編譯器擦除所有類型聲明替換為Object。編譯器自動在核實的地方插入轉換語句。這么做的原因是,泛型代碼和不支持泛型的遺留代碼可以互操作。類型擦除的泛型類型的主要問題是,泛型類型信息在運行時通過反射或運行時類型標識不可用。並且對於這種方式,泛型類型數據結構必須使用對象和非基元類型進行生命。所以只能有Stack<Integer>而不是Stack<int>。
在C#中,.NET運行時中間語言IL直接支持泛型。泛型在編譯的時候,生成的IL包含具體類型的占位符。在運行的時候,如果初始化一個泛型類型的引用,系統會看是否有人已經用過這個類型了,如果類型之前請求過則返回之前生成的具體類型,如果沒有JIT編譯器把泛型參數替換為IL中的具體類型然后再初始化新的類型。如果請求的類型是引用類型而不是值類型的話,泛型類型參數會替換為Object,但是不需要進行轉換,因為.NET運行時會在訪問的時候內部進行轉換。
在某些時候,我們的方法需要操作包含任意類型的數據結構而不是某個具體類型,但是又希望利用強類型泛型的優勢。這個時候我們可以通過C#的泛型類型推斷或Java通配類型實現。如下代碼所示:
C# Code using System; using System.Collections; using System.Collections.Generic; class Test{ //Prints the contents of any generic Stack by //using generic type inference public static void PrintStackContents<T>(Stack<T> s){ while(s.Count != 0){ Console.WriteLine(s.Pop()); } } public static void Main(String[] args){ Stack<int> s2 = new Stack<int>(); s2.Push(4); s2.Push(5); s2.Push(6); PrintStackContents(s2); Stack<string> s1 = new Stack<string>(); s1.Push("One"); s1.Push("Two"); s1.Push("Three"); PrintStackContents(s1); } } Java Code import java.util.*; class Test{ //Prints the contents of any generic Stack by //specifying wildcard type public static void PrintStackContents(Stack<?> s){ while(!s.empty()){ System.out.println(s.pop()); } } public static void main(String[] args){ Stack <Integer> s2 = new Stack <Integer>(); s2.push(4); s2.push(5); s2.push(6); PrintStackContents(s2); Stack<String> s1 = new Stack<String>(); s1.push("One"); s1.push("Two"); s1.push("Three"); PrintStackContents(s1); } }
C#和Java都提供了泛型約束。對於C#有三種類型的約束:
1)派生約束,告訴編譯器泛型類型參數需要從某個基類型繼承,比如接口或基類
2)默認構造方法約束,告訴編譯器泛型類型參數需要提供公共的默認構造方法
3)引用、值類型約束,泛型類型參數需要是引用類型或值類型
對於Java支持派生約束:
C# Code using System; using System.Collections; using System.Collections.Generic; public class Mammal { public Mammal(){;} public virtual void Speak(){;} } public class Cat : Mammal{ public Cat(){;} public override void Speak(){ Console.WriteLine("Meow"); } } public class Dog : Mammal{ public Dog(){;} public override void Speak(){ Console.WriteLine("Woof"); } } public class MammalHelper<T> where T: Mammal /* derivation constraint */, new() /* default constructor constraint */{ public static T CreatePet(){ return new T(); } public static void AnnoyNeighbors(Stack<T> pets){ while(pets.Count != 0){ Mammal m = pets.Pop(); m.Speak(); } } } public class Test{ public static void Main(String[] args){ Stack<Mammal> s2 = new Stack<Mammal>(); s2.Push(MammalHelper<Dog>.CreatePet()); s2.Push(MammalHelper<Cat>.CreatePet()); MammalHelper<Mammal>.AnnoyNeighbors(s2); } }
Java Code import java.util.*; abstract class Mammal { public abstract void speak(); } class Cat extends Mammal{ public void speak(){ System.out.println("Meow"); } } class Dog extends Mammal{ public void speak(){ System.out.println("Woof"); } } public class Test{ //derivation constraint applied to pets parameter public static void AnnoyNeighbors(Stack<? extends Mammal> pets){ while(!pets.empty()){ Mammal m = pets.pop(); m.speak(); } } public static void main(String[] args){ Stack<Mammal> s2 = new Stack<Mammal>(); s2.push(new Dog()); s2.push(new Cat()); AnnoyNeighbors(s2); } }
C#還提供了default操作符可以返回類型的默認值。引用類型的默認值是null,值類型(比如int、枚舉和結構)的默認值是0(0填充結構)。對於泛型default很有用,如下代碼演示了這個操作符的作用:
C# Code using System; public class Test{ public static T GetDefaultForType<T>(){ return default(T); //return default value of type T } public static void Main(String[] args){ Console.WriteLine(GetDefaultForType<int>()); Console.WriteLine(GetDefaultForType<string>()); Console.WriteLine(GetDefaultForType<float>()); } }
輸出:
0
0
15、for-each循環
for-each循環是很多腳本語言、編譯工具、方法類庫中非常常見的一種迭代結構。for-each循環簡化了C#中實現System.Collections.IEnumerable接口或Java中java.lang.Iterable接口數組或類的迭代。
在C#中通過foreach關鍵字來創建for-each循環,而在Java中通過操作符:來實現。
C# Code string[] greek_alphabet = {"alpha", "beta", "gamma", "delta", "epsilon"}; foreach(string str in greek_alphabet) Console.WriteLine(str + " is a letter of the greek alphabet"); Java Code String[] greek_alphabet = {"alpha", "beta", "gamma", "delta", "epsilon"}; for(String str : greek_alphabet) System.out.println(str + " is a letter of the greek alphabet");
16、元數據注解
元數據注解提供了一種強大的方式來擴展編程語言和語言運行時的能力。注解是可以要求運行時進行一些額外任務、提供類額外信息擴展功能的一些指令。元數據注解在許多編程環境中很常見,比如微軟的COM和linux內核。
C#特性提供了為模塊、類型、方法或成員變量增加注解的方式。如下描述了一些.NET自帶的特性以及如何使用它們來擴展C#的能力:
1)[MethodImpl(MethodImplOptions.Synchronized)] 用於指定多線程訪問方法的時候使用鎖進行保護,和Java的sychronized一樣。
2)[Serializable]用於把類標記為可序列化的,和Java的實現Serializable接口相似。
3)[FlagsAttribute]用於指定枚舉支持位操作。這樣枚舉就可以有多個值。
C# Code //declaration of bit field enumeration [Flags] enum ProgrammingLanguages{ C = 1, Lisp = 2, Basic = 4, All = C | Lisp | Basic } aProgrammer.KnownLanguages = ProgrammingLanguages.Lisp; //set known languages ="Lisp" aProgrammer.KnownLanguages |= ProgrammingLanguages.C; //set known languages ="Lisp C" aProgrammer.KnownLanguages &= ~ProgrammingLanguages.Lisp; //set known languages ="C" if((aProgrammer.KnownLanguages & ProgrammingLanguages.C) > 0){ //if programmer knows C //.. do something }
4)[WebMethod]在ASP.NET中用於指定方法可以通過Web服務訪問。
可以通過反射來訪問模塊、類、方法或字段的特性(詳見http://msdn.microsoft.com/zh-cn/library/system.attributetargets.aspx)。對於運行時獲取類是否支持某個行為特別有用,開發者可以通過繼承System.Attribute類來創建他們自己的特性。如下是使用特性來提供類作者信息的例子,然后我們通過反射來讀取這個信息。
C# Code using System; using System.Reflection; [AttributeUsage(AttributeTargets.Class)] public class AuthorInfoAttribute: System.Attribute{ string author; string email; string version; public AuthorInfoAttribute(string author, string email){ this.author = author; this.email = email; } public string Version{ get{ return version; } set{ version = value; } } public string Email{ get{ return email; } } public string Author{ get{ return author; } } } [AuthorInfo("Dare Obasanjo", "kpako@yahoo.com", Version="1.0")] class HelloWorld{ } class AttributeTest{ public static void Main(string[] args){ /* Get Type object of HelloWorld class */ Type t = typeof(HelloWorld); Console.WriteLine("Author Information for " + t); Console.WriteLine("================================="); foreach(AuthorInfoAttribute att in t.GetCustomAttributes(typeof(AuthorInfoAttribute), false)){ Console.WriteLine("Author: " + att.Author); Console.WriteLine("Email: " + att.Email); Console.WriteLine("Version: " + att.Version); }//foreach }//Main }
Java的提供了為包、類型、方法、參數、成員或局部變量增加注解的能力。Java語言只內置了三種注解,如下:
1)@Override用於指定方法覆蓋基類的方法。如果注解的方法並沒有覆蓋基類的方法,在編譯的時候會出錯。
2)@Deprecated用於指示某個方法已經廢棄。使用廢棄的方法會在編譯的時候產生警告。
3)@SuppressWarnings用於防止編譯器發出某個警告。這個注解可選接收參數來表明屏蔽哪種警告。
和C#一樣,可以通過反射來讀取模塊、類、方法或字段的注解。但是不同的是Java注解可以是元注解。開發者可以創建自定義的注解,創建方法和接口相似,只不過需要定義@interface關鍵字。如下是使用注解和通過反射讀取信息的例子:
Java Code import java.lang.annotation.*; import java.lang.reflect.*; @Documented //we want the annotation to show up in the Javadocs @Retention(RetentionPolicy.RUNTIME) //we want annotation metadata to be exposed at runtime @interface AuthorInfo{ String author(); String email(); String version() default "1.0"; } @AuthorInfo(author="Dare Obasanjo", email="kpako@yahoo.com") class HelloWorld{ } public class Test{ public static void main(String[] args) throws Exception{ /* Get Class object of HelloWorld class */ Class c = Class.forName("HelloWorld"); AuthorInfo a = (AuthorInfo) c.getAnnotation(AuthorInfo.class); System.out.println("Author Information for " + c); System.out.println("======================================="); System.out.println("Author: " + a.author()); System.out.println("Email: " + a.email()); System.out.println("Version: " + a.version()); } }
17、枚舉
枚舉用於創建一組用於自定義的命名常量。盡管從表面上看C#和Java的枚舉非常相同,但是在實現上它們完全不同。Java的枚舉類型是純種的類,也就是它們是類型安全並可以增加方法、字段或甚至實現接口進行擴展,而在C#中枚舉純粹是一個包裝了數字類型(一般是int)的語法糖,並且不是類型安全的。如下代碼演示了兩者的區別:
C# Code using System; public enum DaysOfWeek{ SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY } public class Test{ public static bool isWeekDay(DaysOfWeek day){ return !isWeekEnd(day); } public static bool isWeekEnd(DaysOfWeek day){ return (day == DaysOfWeek.SUNDAY || day == DaysOfWeek.SATURDAY); } public static void Main(String[] args){ DaysOfWeek sun = DaysOfWeek.SUNDAY; Console.WriteLine("Is " + sun + " a weekend? " + isWeekEnd(sun)); Console.WriteLine("Is " + sun + " a week day? " + isWeekDay(sun)); /* Example of how C# enums are not type safe */ sun = (DaysOfWeek) 1999; Console.WriteLine(sun); } }
Java Code enum DaysOfWeek{ SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY; public boolean isWeekDay(){ return !isWeekEnd(); } public boolean isWeekEnd(){ return (this == SUNDAY || this == SATURDAY); } } public class Test{ public static void main(String[] args) throws Exception{ DaysOfWeek sun = DaysOfWeek.SUNDAY; System.out.println("Is " + sun + " a weekend? " + sun.isWeekEnd()); System.out.println("Is " + sun + " a week day? " + sun.isWeekDay()); } }
運行結果:
Is SUNDAY a weekend? true
Is SUNDAY a week day? false
第三部分:C#中有,Java中也有但完全不同的地方
1、嵌套類
在Java和C#中可以在一個類中嵌套另一個。在Java中有兩種類型的嵌套類,非靜態的嵌套類也就是內部類,以及靜態的嵌套類。Java的內部類可以認為是內部類和其包含類一對一的關系,每一個包含類的實例都保存了一個相應內部類的實例,內部類可以額訪問包含類的成員變量以及包含非靜態的方法。Java的靜態內部類可以訪問包含類的靜態成員和方法。C#也有Java的靜態嵌套類,但是沒有Java的內部類。如下嵌套類聲明是等價的:
C# Code public class Car{ private Engine engine; private class Engine{ string make; } } Java Code public class Car{ private Engine engine; private static class Engine{ String make; } }
注意:在Java中,嵌套類可以在任何代碼塊中聲明,包括方法,在C#中則不能。在方法中創建嵌套類看上去不必要,但是結合匿名內部類可以提供強大的設計模式。
2、線程和易失成員
線程是程序內的順序控制流程。程序或進程可以有多個線程並行執行,在執行任務的時候可以共享數據也可以獨立運行。這樣開發人員就可以在一個程序或進程中一次執行多個任務。線程的優點包括充分利用多處理器架構的資源、通過一邊處理任務一邊等待阻塞的系統調用(比如打印機或其它IO)減少執行時間,避免GUI應用程序失去響應。
Java線程可以通過繼承java.lang.Thread並重寫run()方法或者實現java.lang.Runable接口並實現run()方法來實現。在C#中,可以創建System.Threading.Thread對象並且傳入System.Threading.Thread委托來表示需要線程運行的方法。在Java中,每一個類都從java.lang.Object繼承wait()、notify()以及notify()。在C#中的等價是Thread.Threading.Montir類的Wait()、Pulse()以及PulseAll()。
using System; using System.Threading; using System.Collections; public class WorkerThread { private int idNumber; private static int num_threads_made = 1; private ThreadSample owner; public WorkerThread(ThreadSample owner) { idNumber = num_threads_made; num_threads_made++; this.owner = owner; }/* WorkerThread() */ //sleeps for a random amount of time to simulate working on a task public void PerformTask() { Random r = new Random((int)DateTime.Now.Ticks); int timeout = (int)r.Next() % 1000; if (timeout < 0) timeout *= -1; //Console.WriteLine(idNumber + ":A"); try { Thread.Sleep(timeout); } catch (ThreadInterruptedException e) { Console.WriteLine("Thread #" + idNumber + " interrupted"); } //Console.WriteLine(idNumber + ":B"); owner.workCompleted(this); }/* performTask() */ public int getIDNumber() { return idNumber; } } // WorkerThread public class ThreadSample { private static Mutex m = new Mutex(); private ArrayList threadOrderList = new ArrayList(); private int NextInLine() { return (int)threadOrderList[0]; } private void RemoveNextInLine() { threadOrderList.RemoveAt(0); //all threads have shown up if (threadOrderList.Count == 0) Environment.Exit(0); } public void workCompleted(WorkerThread worker) { try { lock (this) { while (worker.getIDNumber() != NextInLine()) { try { //wait for some other thread to finish working Console.WriteLine("Thread #" + worker.getIDNumber() + " is waiting for Thread #" + NextInLine() + " to show up."); Monitor.Wait(this, Timeout.Infinite); } catch (ThreadInterruptedException e) { } }//while Console.WriteLine("Thread #" + worker.getIDNumber() + " is home free"); //remove this ID number from the list of threads yet to be seen RemoveNextInLine(); //tell the other threads to resume Monitor.PulseAll(this); } } catch (SynchronizationLockException) { Console.WriteLine("SynchronizationLockException occurred"); } } public static void Main(String[] args) { ThreadSample ts = new ThreadSample(); /* Launch 25 threads */ for (int i = 1; i <= 25; i++) { WorkerThread wt = new WorkerThread(ts); ts.threadOrderList.Add(i); Thread t = new Thread(new ThreadStart(wt.PerformTask)); t.Start(); } Thread.Sleep(3600000); //wait for it all to end }/* main(String[]) */ }
Java Code import java.util.*; class WorkerThread extends Thread{ private Integer idNumber; private static int num_threads_made = 1; private ThreadSample owner; public WorkerThread(ThreadSample owner){ super("Thread #" + num_threads_made); idNumber = new Integer(num_threads_made); num_threads_made++; this.owner = owner; start(); //calls run and starts the thread. }/* WorkerThread() */ //sleeps for a random amount of time to simulate working on a task public void run(){ Random r = new Random(System.currentTimeMillis()); int timeout = r.nextInt() % 1000; if(timeout < 0) timeout *= -1 ; try{ Thread.sleep(timeout); } catch (InterruptedException e){ System.out.println("Thread #" + idNumber + " interrupted"); } owner.workCompleted(this); }/* run() */ public Integer getIDNumber() {return idNumber;} } // WorkerThread public class ThreadSample{ private Vector threadOrderList = new Vector(); private Integer nextInLine(){ return (Integer) threadOrderList.firstElement(); } private void removeNextInLine(){ threadOrderList.removeElementAt(0); //all threads have shown up if(threadOrderList.isEmpty()) System.exit(0); } public synchronized void workCompleted(WorkerThread worker){ while(worker.getIDNumber().equals(nextInLine())==false){ try { //wait for some other thread to finish working System.out.println (Thread.currentThread().getName() + " is waiting for Thread #" + nextInLine() + " to show up."); wait(); } catch (InterruptedException e) {} }//while System.out.println("Thread #" + worker.getIDNumber() + " is home free"); //remove this ID number from the list of threads yet to be seen removeNextInLine(); //tell the other threads to resume notifyAll(); } public static void main(String[] args) throws InterruptedException{ ThreadSample ts = new ThreadSample(); /* Launch 25 threads */ for(int i=1; i <= 25; i++){ new WorkerThread(ts); ts.threadOrderList.add(new Integer(i)); } Thread.sleep(3600000); //wait for it all to end }/* main(String[]) */ }//ThreadSample
在許多情況下,我們不能確保代碼執行的順序和源代碼一致。產生這種情況的原因包括編譯器優化的時候重排語句順序、或多處理器系統不能在全局內存中保存變量。要避免這個問題,C#和Javay引入了volatile關鍵字來告訴語言運行時不要重新調整字段的指令順序。
C# Code /* Used to lazily instantiate a singleton class */ /* WORKS AS EXPECTED */ class Foo { private volatile Helper helper = null; public Helper getHelper() { if (helper == null) { lock(this) { if (helper == null) helper = new Helper(); } } return helper; } } Java Code /* Used to lazily instantiate a singleton class */ /* BROKEN UNDER CURRENT SEMANTICS FOR VOLATILE */ class Foo { private volatile Helper helper = null; public Helper getHelper() { if (helper == null) { synchronized(this) { if (helper == null) helper = new Helper(); } } return helper; } }
盡管上面的代碼除了lock和synchronized關鍵字之外沒什么不同,Java的版本不保證在所有的JVM下都工作,詳見http://www.javaworld.com/javaworld/jw-02-2001/jw-0209-double.html。在C#中volatile的語義不會出現這樣的問題,因為讀寫次序不能調整,同樣被標記volatile的字段不會保存在寄存器中,對於多處理器系統確保變量保存在全局內存中。
3、操作符重載
操作符重載允許特定的類或類型對於標准操作符具有新的語義。操作符重載可以用於簡化某個常用操作的語法,比如Java中的字符串連接。操作符重載也是開發人員爭議的一個地方,它在帶來靈活性的同時也帶來了濫用的危險。有些開發者會亂用重載(比如用++或--來表示連接或斷開網絡)或是讓操作符不具有本來的意義(比如[]返回一個集合中某個索引項的復制而不是原始的對象)或重載了一半操作符(比如重載<但是不重載>)。
注意,和C++不用,C#不允許重載如下的操作符:new、()、||、&&、=或各種組合賦值,比如+=、-=。但是,重載的組合賦值會調用重載的操作符,比如+=會調用重載的+。
C# Code using System; class OverloadedNumber{ private int value; public OverloadedNumber(int value){ this.value = value; } public override string ToString(){ return value.ToString(); } public static OverloadedNumber operator -(OverloadedNumber number){ return new OverloadedNumber(-number.value); } public static OverloadedNumber operator +(OverloadedNumber number1, OverloadedNumber number2){ return new OverloadedNumber(number1.value + number2.value); } public static OverloadedNumber operator ++(OverloadedNumber number){ return new OverloadedNumber(number.value + 1); } } public class OperatorOverloadingTest { public static void Main(string[] args){ OverloadedNumber number1 = new OverloadedNumber(12); OverloadedNumber number2 = new OverloadedNumber(125); Console.WriteLine("Increment: {0}", ++number1); Console.WriteLine("Addition: {0}", number1 + number2); } } // OperatorOverloadingTest
4、switch語句
C#的switch和Java的switch有兩個主要的區別。在C#中,switch語句支持字符串常量,除非標簽不包含任何語句否則不允許貫穿。貫穿是顯式禁止的,因為可能導致難以找到的bug。
C# Code switch(foo){ case "A": Console.WriteLine("A seen"); break; case "B": case "C": Console.WriteLine("B or C seen"); break; /* ERROR: Won't compile due to fall-through at case "D" */ case "D": Console.WriteLine("D seen"); case "E": Console.WriteLine("E seen"); break; }
5、程序集
C#程序集和Java的JAR文件有很多共性。程序集是.NET環境最基本的代碼打包單元。程序集包含了中間語言代碼、類的元數據以及其它執行任務所需要的數據。由於程序集是最基本的打包單元,和類型相關的一些行為必須在程序集級別進行。例如,安全授權、代碼部署、程序集級別的版本控制。Java JAR文件有着相似的作用,但是實現不一樣。程序集一般是EXE或DLL而JAR文件是ZIP文件格式的。




