简介
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文件格式的。