下面让我们考察 Java 语言和 C# 之间的区别。这两种语言具有许多相似之处,因此我们将重点关注其区别所在。
类型
Java 语言和 C# 都是单继承的面向对象语言,它们都具有一个作为其他所有类的基类的类:C# 中的 System.object 和 Java 语言中的 java.lang.Object 。这意味着对于您开发的类层次,两种语言是相似的。如果沿着层次树往上,您最终会到达对应的根类。
Java 语言使用了原始类型(primitive type)的概念,它们非常类似 C 和 C++ 中的对应概念。它们不是任何类层次的一部分,也不具有任何方法。此外,当使用它们作为参数时,它们总是按值传递。表 3 列出了 Java 语言中的原始类型和它们在 C# 中的等价类型:
表 3 Java 语言中的原始类型和它们在 C# 中的等价类型
Java 原始类型 | 描述 | 等价的 C# 类型 | 描述 |
int | 32 位有符号整数 | int | 32 位有符号整数 |
long | 64 位有符号整数 | long | 32 位有符号整数 |
short | 16 位有符号整数 | short | 16 位有符号整数 |
char | 16 位无符号整数 | char | 16 位无符号整数 |
byte | 8 位无符号整数 | byte | 8 位无符号整数 |
boolean | 有效值是 true或者 false | bool | 有效值是 true或者 false |
float | 32 位浮点数 | float | 32 位浮点数 |
double | 64 位浮点数 | double | 64 位浮点数 |
清单 12. 原始类型和它们的包装类的例子
2 public class Foo public class Foo
3 { {
4 private void someMethod(object arg) { private void someMethod(Object arg) {
5 // do something with arg // do something with arg
6 } }
7 public static void Main(string[] args) { public static void main(String[] args) {
8 int i = 0; int i=0;
9 Foo x = new Foo(); Foo x = new Foo();
10 x.someMethod(i); x.someMethod(new Integer(i));
11 } }
12 } }
13
清单 12 中的 Java 原始类型被显式地包装在 Object 的一个派生类中,而在 C# 中,这种转换是隐式的。(C# 中隐式的转换称为 装箱(boxing)。)
注意在 Java 语言中,原始类型是按值传递的,对象类型在内部使用指针来表示,它们也是按值传递的。C# 中的默认行为是相同的,不过该语言还包括 ref 关键字来强制将值类型作为引用来传递。
表 4 显示了一些等价于 C# 类型但是没有映射到 Java 原始类型的 Java 语言类型。
表 4 C# 类型和它们的 Java 等价类型
2 java.lang.Object 任何非原始类型都是 Object 的派生类 object 每种类型都是 object 的派生类
3 java.lang.String Unicode 字符 string Unicode 字符
4 java.math.BigDecimal 可调整进位制的 32 位小数 decimal 可调整进位制的 32 位小数
5
继承和接口
两种语言都仅允许单继承,但是都允许实现多个接口。这在两种语言中的实现方式有所不同。例如,清单 13 显示了如何从一个名为 Parent 的类派生子类,并实现两个分别名为 IOne 和 ITwo 的接口。
清单 13 派生子类并实现接口
2 public class Child : Parent, IOne, ITwo public class Child extends Parent implements IOne, ITwo
3 { {
4 ... ...
5 } }
6
7
8
注意在 Java 语言中, extends 关键字表示继承, implements 关键字表示一个或多个接口的实现。
包
如果熟悉 C# 中的名称空间,那么对 Java 语言中的包就不应该有任何概念问题。像名称空间一样,包允许您组织类以避免当您在不同上下文中使用相同名称的类时存在的名称冲突。名称空间的逻辑类分组促进了类复用,使得导航大量的类成员更加容易。在 Java 语言中,您需要通过两种方式处理这种分组:如何将类声明为特定包的成员,以及如何引用特定包中的类。清单 14 中的例子说明了名称空间和包的处理。
清单 14. 名称空间和包
2 // Foo will be in this namespace // Foo will be in this package
3 namespace MyApp.Utilities package com.mycompany.myapp.utilities;
4 public class Foo public class Foo
5 { {
6 ... ...
7 } }
8 // using Foo in another class // using Foo in another class
9 using MyApp.Utilities.Foo; import com.mycompany.myapp.utilities.Foo;
10 public class Foo2 public class Foo2
11 { {
12 ... ...
13 } }
14
注意在 Java 语言中,约定的做法是包名称全部使用小写,并使用反向的 Internet 域名作为每个包的前缀。上述清单使用 Java import 语句(类似 C# using 语句)来引用 Foo2 中的 Foo 类,而没有使用完全限定的名称(C# 中的 MyApp.Utilities.Foo 或 Java 语言中的 com.mycompany.myapp.utilities.Foo )。
打包以供复用
在 C# 中,您可以编写一组类,把它们生成程序集(assembly),程序集在文件系统中表示为动态链接库。其他类可以引用这些程序集,以便使用它们所包含的类。Java 语言也允许把一组类打包到一个称为 Java 归档(Java Archive,JAR)文件的文件中以供复用。您可以将一组类组合到一个带有 .jar 扩展名的文件中,然后在其他类中引用该 JAR 文件。具有 .jar 扩展名的文件是标准的 zip 文件,可以使用 WinZip 或其他压缩实用程序来操作它们。不过为方便起见,Java SDK 包含了一个名为 jar.exe 的实用程序(在 Windows 平台上),可以使用它来把一组类组合到一个具有 .jar 扩展名的文件中。
在考察使用 jar.exe 实用程序的例子之前,理解包名称和 Java 平台用于生成类以及在运行时加载它们的目录结构之间的关系是很重要的。请考虑一个名为 Test 的类,它的源代码在一个名为 Test.java 的文件中。如果将 Test.java 定义为包 com.mycompany.test 的一部分,那么编译器会为结果类模块创建一个目录树。该目录树就建立在包名称的基础上。此例中该目录树为 com\mycompany\test,并且包名称中的点号被转换为目录边界(分隔符 \)。
现在打开一个命令提示符窗口,然后创建一个目录(例如 c:\javapack)。切换到该目录( cd javapack )。使用您最喜欢的文本编辑器,将清单 15 中的代码添加到一个名为 Test.java 的新文件中。
清单 15. 使用包的例子
2 public class Test
3 {
4 public static void main(String[] args) {
5 System.out.println("In test");
6 }
7 }
8
使用以下命令编译 Test.java。(注意 -d 选项应该指向您为这个例子创建的目录):
2
现在 c:\javapack 目录下应该有一个名为 com 的子目录。事实上,您可以看到编译所产生的 comTest.class 文件的完全限定名称是 Test.class。 注意包名称( com.mycompany.test )是如何转换为对应目录结构(com\mycompany\test)的,该目录结构以您使用 -d 选项指定的目录作为根目录。
下面我们将展示如何打包 Test.class 以方便其他类复用。从 c:\javapack 目录运行以下命令:
2
jar 命令将创建一个名为 Test.jar 的文件,它包含 com 子目录下的所有类。
执行以下命令来使用 Test.jar 文件中的类:
2
注意您必须使用完全限定的类名称来从命令行运行该命令,而且还要注意使用 -classpath 选项来指向 Test.jar 文件的方式。或者,您可以把 Test.jar 文件添加到 CLASSPATH 环境变量中,该变量是分号分隔的 JAR 文件列表,以及 Java 编译器和 Java 虚拟机(Java virtual machine,JVM)用来寻找需要加载的类的目录列表。
访问修饰符
访问修饰符 public 、 private 和 protected 在两种语言中的工作方式大部分都是相同的。在 C# 中,默认的访问权限是 private ;Java 语言中的默认行为是允许任何子类或相同包中的任何类访问当前类、字段或方法。这大致等价于 C# 中的 internal 关键字,该修饰符仅允许从相同程序集访问。
方法重载
C# 中的子类如果要重载父类中的某个方法:
该方法一定 不 能在父类中使用 private 访问修饰符(或没有访问修饰符)来声明。
该方法必须在父类中声明为 virtual 。
子类中的重载方法必须与父类中的方法具有相同的名称、返回类型和参数签名。
子类中的方法必须使用 Overrides 关键字来声明。(您也可以使用 new 关键字,但是一般不推荐这样做。)
父类中的方法一定 不 能声明为 sealed 。
Java 语言中方法重载的前提条件不太严格:
该方法一定 不 能在父类中使用 private 访问修饰符来声明。
子类中的方法必须与父类中的对应方法具有相同的名称、返回类型和参数签名。
父类中的方法一定 不 能声明为 final 。
这些区别的含义是,在 Java 代码中,子类中不可能包含与父类中的非私有方法具有相同名称和签名的方法而不会隐式地重载它。在 C# 中,您必须显式地指明何时想要重载父类中的方法。还要注意 Java 语言中的 final 关键字如何或多或少地等价于 C# 中的 sealed 关键字。
异常处理
结构化的异常处理在两种语言中几乎完全相同。(两者都可以往后追溯到一份创始性的论文,即 Andrew Koenig 和 Bjarne Stroustrup 于 1990 年撰写的 Exception Handling for C++。) 两种语言都使用了两种异常概念:应用程序生成的异常,以及系统运行库(C# 的公共语言运行库,Java 语言的 JVM)生成的异常。两种语言都具有 Exception 基类,应用程序异常和系统异常都是由它派生而来的。 图 3 显示了两种语言中的 Exception 类层次。
图3. Java 语言和 C# 中的 Exception 类层次
然而,两种语言的编译器对您的代码如何处理异常具有不同的预期。在 C# 中,您可以选择捕获异常,或让它们沿调用堆栈向上传播到类的方法的调用者(和/或构造函数)。Java 语言允许同样的处理,但是对于未捕获的应用程序异常(也就是 java.lang.Exception 的子类),您必须显式地将它们作为方法声明的一部分来列出。因此 Java 编译器预期您或者自己捕获所有应用程序异常,或者告诉编译器关于未捕捉的异常的信息。例如,假设 Foo 类的构造函数可能抛出一个应用程序异常,那么清单 16 中的 C# 或 Java 代码对各自的编译器来说都不会有问题。
清单 16. 处理应用程序异常
2 ... ...
3 public void someMethod() { public void someMethod() {
4 try { try {
5 Foo x = new Foo(); Foo x = new Foo();
6 } }
7 catch (ApplicationException e) catch (Exception e)
8 { {
9 ... ...
10 } }
11 } }
12
然而,如果改变代码以使其不捕获异常,那么您就必须改变 Java 方法声明(如清单 17 所示)以避免编译器错误。
清单 17. 未捕获的应用程序异常
2 ... ...
3 public void someMethod() { public void someMethod() throws Exception {
4 Foo x = new Foo(); Foo x = new Foo();
5 } }
6
两种语言在这方面存在的另一个区别在于,在 C# 中,每个捕获块的参数是可选的。如果省略它,那么所有异常都会被捕获。Java 语言不允许这样,而是允许一个等价功能( java.lang.Throwable ),它捕获所有异常类的父类,如清单 18 所示。
清单 18. 捕获所有异常
2 ... ...
3 public void someMethod() { public void someMethod() {
4 try { try {
5 Foo x = new Foo(); Foo x = new Foo();
6 } }
7 catch catch (Throwable e)
8 { {
9 ... ...
10 } }
11 } }
12
数组声明
Java 语言提供两种声明数组的方法;C# 仅提供了一种方法。清单 19 中的代码说明了这个区别。
清单 19. 声明数组
2 ... ...
3 // This is how an array is declared in C# // In Java the following are both allowed
4 private int[] myArray; private int[] myArray;
5 // or
6 private int myArray[];
7
委托和索引器(indexer)
Java 语言没有直接等价于 C# 委托的结构。您可以通过声明并实现一个具有单个方法定义的接口来模拟委托功能。
Java 语言也没有索引器;您需要将它们编写为常规的类方法。
操作符重载
Java 语言不允许操作符重载(这是一个从 C++ 借用来的 C# 特性)。您可以容易地编写方法来模拟操作符重载行为。
非安全模式
C# 的非安全模式允许您使用指针和内存插接块(pin bolck)来绕过垃圾收集。Java 运行库本身广泛使用了指针,但是 Java 语言没有指针,也没有等价的非安全模式。这样是为了遵循 Java 平台的“编写一次,随处运行”的哲学,它允许你安全地避免平台依赖性、内存泄露以及“失控(runaway)”代码。
小结
您的 C# 背景应该使得转向 Java 平台相当容易。本文中手把手的代码示例或许会让您认识到:这两种语言使用了相当类似的语法。它们在概念上也相当相似。继承、接口和异常处理就是这两种语言的实现几乎完全相同的一些方面。我们提倡您首先把一些 C# 小程序转换到 Java 语言。不要忘了使用 Java 平台文档,要查找功能上等价于 System... 名称空间中的 C# 类的 Java 类,您会发现这些文档非常有用。