目录
- 介绍(Introduction)
- 相似点(Similarities)
- 编译单位(Compiled Units)
- 命名空间(Namespaces)
- 顶层成员(类型)(Top Level Elements(Types))
- 基础类型(Basic Types)
- 类(Classes)
- 结构体(Structures)
- 接口(Interfaces)
- 泛型(Generic Types)
- 委托(Delegates)
- 枚举(Enumerations)
- 类型访问级别(Type Visibilities)
- 继承(Inheritance)
- 嵌套类(Inner Classes)
- 抽象类(Abstract Classes)
- 密封类(Sealed Classes)
- 静态类(Static Classes)
- 可空类型(Nullable Types)
- 部分类(Partial Classes)
- 匿名类(Anonymous Classes)
- 类型成员(Type Members)
- 静态构造方法(Static Constructors)
- 析构方法(Destructors)
- 静态成员(Static Members)
- 属性(Properties)
- 事件(Events)
- 字段与属性的自动初始化(Automatic Initialization of Fields and Properties)
- 成员访问级别(Member Visibilities)
- 虚拟成员(Virtual Members)
- 密封成员(Sealed Members)
- 抽象成员(Abstract Members)
- 泛型成员(Generic Members)
- 只读字段(Read-Only and Constant Fields)
- 技术审阅(Technical Review)
我很多朋友或者同事都以为我不喜欢使用Java,他们都认为我是一个纯粹的.NET技术爱好者,其实事实并不是这样子的:)。我也喜欢使用Java,并已经从事Java开发很多年了。
网上已经有很多有关C#与Java之间“异同”的文章,但是我认为并没有哪一篇文章的内容让我非常满意。在接下来我的几篇博客里,我将会竭尽全力地去阐述它们两者之间的“异同之处”。虽然我并不想挑起“口水仗”,但是我还是会以我的角度去阐述谁好谁不好(毫不留情地)。我尽可能地覆盖各个方面,但是并不会去深入比较两者的API,相反我会将重点放在各自语言特性上。
第一篇博客主要集中在各自的顶层结构上,比如命名空间、类型等。这里两者的版本分别是Java8和C#5.0。
两种语言都是大小写敏感的,严格意义上的“面向对象语言”,支持类、枚举以及接口,并且只允许单继承,所以的类型定义必须放在命名空间(namespace/package)中。同时,都支持注释,方法以及字段(包括静态的)。两者的最基本类型均是Object。两者都有相同的基本运算符、相似的异常处理机制。两者的程序启动方法均是一个名叫Main/main的静态方法。
一个Java类编译之后会生成一个class文件,这些文件虽然可以单独存在,但是实际中它们通常和一些清单文件一起被打包进jar,war或者ear文件中,这样做是为了方便管理。jar(或其他格式)文件中还可以包含一些其他资源,比如图片,文本文件等。
C#类通常存在于一个程序集中,程序集有两种格式:
- dll:一个库,不能独自运行;
- exe:一个可以独自运行的可执行文件,可以是一个Console程序,或者Winform,也可以是一个wpf程序。
程序集同样可以包含一些元数据、嵌入的资源。C#/.NET中其实还定义了另外一个编译单元:模块(module)。但是通常情况下,一个module匹配一个assembly。
C#和Java中都有namespace和package的概念。在C#中,namespace必须在它所有其他类型的最外部,一个源文件中,可以存在多个namespace,甚至嵌套形式的。
1 namespace MyNamespace1 2 { 3 public class Class1 4 { 5 } 6 } 7 namespace MyNamespace2 8 { 9 public class Class2 10 { 11 } 12 namespace MyNamespace3 13 { 14 public class Class3 15 { 16 } 17 } 18 }
在Java中,package的定义在源文件最顶部位置,换句话说,一个源文件只能有一个package定义。
1 package myPackage; 2 public class MyClass 3 { 4 }
这里有一个非常大的不同之处,在Java中,package的定义必须与物理文件目录一致,也就是说,一个类如果属于a.b 这个package,那么这个类文件必须存在于a\b这个目录下,否则,编译不会通过。编译产生的.class文件也必须放在同一个目录之下,比如a\b\MyClass.class。
Java和C#中都可以通过导入命名空间的方式去访问其他命名空间的类型,比如Java中可以这样导入所有类型(使用*匹配所有类型),也可以一次导入一个。
1 import java.io.*; 2 import java.lang.reflect.Array;
C#中不支持单独导入一个类型,只能导入命名空间中的全部类型。
1 using System; 2 using System.IO;
不过C#中允许我们使用using关键字为一个类型定义一个别名。
1 using date = System.DateTime; 2 public class MyClass 3 { 4 public date GetCurrentDate() 5 { 6 //... 7 } 8 }
这种方式跟单独导入一个类型其实差不多意思。
Java和C#提供相似的语法,从某种程度上讲,如果我们忽略它们是两种不同的语言,那么有时候是难区分它们有什么不同,当然即使这样,它们之间还是有一些重要不同之处的。
Java中提供了以下几种顶级成员,除了package之外:
- 类(包括泛型)
- 接口(包括泛型)
- 枚举
C#中要多几个:
- 类(包括泛型)
- 接口(包括泛型)
- 枚举
- 结构体
- 委托(包括泛型)
两种语言中有以下基础类型(C#/Java):
- Object/object(C#简写:object)
- String/string(C#简写:string)
- Byte/byte(C#简写:byte)
- SByte/N/A(C#简写:sbyte)
- Boolean/boolean(C#简写:bool)
- Char/char(C#简写:char)
- Int16/short(C#简写:short)
- UInt16/N/A(C#简写:unit)
- Int32/int(C#简写:int)
- UInt32/N.A(C#简写:uint)
- Int64/long(C#简写:long)
- UInt64/N/A(C#简写:ulong)
- Single/float(C#简写:float)
- Double/double(C#简写:double)
- Decimal/N/A(C#简写:decimal)
- dynamic/N/A
- Arrays
如你所见,对于所有的整型类型,C#提供有符号和无符号两种,并且还提供精度更高的Decimal类型。(译者注:上面列表中,“/”前面是C#中的结构体写法,Int32其实是System.Int32,每种类型都提供一种简写方式,System.Int32对应的简写方式是int。Java中不存在结构体,只有一种写法)
C#中提供三种数组:
- 一维数组:int[] numbers(每个元素都为int)
- 多维数组:int[,] matrix (每个元素都为int)
- 数组的数组,又叫锯齿数组:int[][] matrix ((int[])[] 每个元素都是一个int[])
Java中也提供一维数组和锯齿数组,但是并没有多维数组。(译者注:在某种意义上讲,锯齿数组可以代替多维数组)
C#允许我们使用var关键字来定义一个变量并且初始化它,这是一种初始化变量的简写方式:
1 var i = 10; //int 2 var s = "string"; //string 3 var f = SomeMethod(); //method's return type, except void
与Java一样,C#同样允许我们在一个数字后面添加后缀来标明它是什么类型的数据:
- 10n:integer
- 10l:long
- 10f:float
- 10d:double
- 10u:unsigned int(仅C#)
- 10ul:unsigned long(仅C#)
- 10m:decimal(仅C#)
大小写均可作为后缀。
C#和Java中,类都分配在堆中。一个类只允许单继承自另外一个类,如果没有指定,默认继承自Object。每个类均可以实现多个接口。(单继承,多实现)
C#中有一套完整的类型系统,也就是说,所有基本类型(比如int、bool等)均和其他类型一样遵循同一套类型规则。这和Java明显不同,在Java中,int和Integer没有关系(虽然它们之间可以相互转换)。在C#中,所有的基本类型均是结构体(非class),它们均分配在栈中。在Java中,基本类型(int、long等)同样分配在栈中,但是它们并不是结构体,同样,Java中我们并不能自己定义一种分配在栈中的数据类型。C#中的结构体不能显式地继承自任何一个类,但是可以实现接口。
1 public struct MyStructure : IMyInterface 2 { 3 public void MyMethod() 4 { 5 6 } 7 }
在C#中,结构体和枚举被称为“值类型”,类和接口被称为“引用类型”。由于C#(.NET)的统一类型系统,结构体隐式继承自System.ValueType。
(译者注:严格意义上讲,Java并非完全面向对象。Java中的类型存在特殊,比如基础类型int、long、bool等,这些类型完全脱离了主流规则。此外,在C#中我们可以定义一种分配在栈中的类型,比如结构体)
在C#中,一个接口可以包含:
- 实例方法声明
- 实例属性声明
- 实例事件声明
当然它们也可以是泛型的。类和结构体均可实现接口,一个接口可以被赋值为NULL,因为它是引用类型。
在Java中,情况有一点不同。因为接口中可以有静态成员、方法的实现:
- 实例方法声明
- 字段(静态)附带一个初始化值
- 默认方法:包含默认实现,使用default关键字标记
它们同样可以是泛型的。在Java中,一个接口中的方法可以存在访问级别,也就是说,不一定总是public。
在Java中如果一个接口仅仅包含一个方法声明(同时可以包含一个或多个“默认方法”),那么这个接口可以被标记为“函数式接口”(Funcitional Interface),它可以用在lambda中,接口中的方法被隐式地调用(参见后面有关委托部分)(译者注:可以将一个lambda表达式赋给函数式接口,然后通过该接口去执行lambda表达式。默认方法、函数式接口、lambda表达式均属于Java8中新增加内容)。
C#和Java中的泛型有很大的不同。虽然两者都支持泛型类、泛型接口等,但是在C#中,泛型得到了更好的支持,而Java中泛型一旦经过编译后,类型参数就不存在了。也就是说在Java中,List<String>在运行阶段就会变成List类型,泛型参数String会被抹去,这样设计主要是为了与Java更老版本进行兼容。Java中的这种情况并不会发生在C#中,C#中我们可以通过反射得到一个泛型类的所有信息,当然也包括它的参数。
两种语言都支持多个泛型参数,并且都有一些限制。C#中的限制如下:
- 基类、结构体、接口:可以强制泛型参数继承自一个特定的类(或实现特定的接口);
- 具备无参构造方法的非抽象类:只允许非抽象并且具备无参构造方法的类型作为泛型参数;
- 引用类型和值类型:泛型参数要么被指定为引用类型(类、接口),要么被指定为值类型(结构体、枚举)。
比如:
1 public class GenericClassWithReferenceParameter<T> where T : class 2 { 3 4 } 5 public class GenericClassWithValueParameter<T> where T : struct 6 { 7 8 } 9 public class GenericClassWithMyClassParameter<T> where T : MyClass 10 { 11 12 } 13 public class GenericClassWithPublicParameterlessParameter<T> where T : new() 14 { 15 16 } 17 public class GenericClassWithRelatedParameters<K, V> where K : V 18 { 19 20 } 21 public class GenericClassWithManyConstraints<T> where T : IDisposable where T : new() where T : class 22 { 23 24 }