七种武器——C# VS Java
原文链接 http://huiming.io/2017/09/25/cs-vs-java.html
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。
这篇文章是“七种武器”系列的一篇,在这篇导言中有相关的介绍和代码,读者应该先读一读它。
目录
- 目录 {:toc}
C#和Java相似:它们都把程序编译成某种“字节码”,然后在某种“虚拟机”上执行该字节码。此外,它们的语法形式都深受C++影响1。另外,它们还是相互竞争的关系。因此,把它们放在一起比较是有意义的。
C# Namespace VS Java Package
它们都是对应语言用于组织名字空间(namespace)的工具,但形式上很不相同:
- Java要求目录结构与Package对应,比如对于Package
io.huiming.hangman
的目录结构必须是io/huiming/hangman
,所有在这个Package下的类型的实现文件都必须位于那个目录下。C#的Namespace则不然:名字空间Io.Huiming.Hangman
下的类型的实现文件可以在任何地方。 - Java通过
import
关键字(keyword)导入一个类型,如import java.util.List;
,或者一个包下的所有类型,如import java.util.*;
;但C#的using
指令(directive)一般用于导入整个Namespace下的类型,如using System
,只有在发生名字冲突的情况下才需要单独导入某个类型,如using Console2 = System.Console
——这条指令同时给System.Console起了一个别名Console2
。
类定义(Class)
C#与Java的类定义语法大同小异,但是C#提供了更多的语言设施,比如属性(Property)和索引(Indexer)。
Java需要通过getter和setter方法这种“约定俗成”的方式来定义属性:
public String getGuessedSoFar() {
return new String(guessedSoFar);
}
在C#中可以更简单:
public string GuessedSoFar => new string(guessedSoFar);
对于有多行代码的属性:
public ISet<char> AllGuessedLetters {
get {
ISet<char> guessed = new HashSet<char>();
guessed.AddAll(correctlyGuessedLetters);
guessed.AddAll(incorrectlyGuessedLetters);
return guessed;
}
}
同时支持读写的属性:
private int count;
public int Count {
get { return count; }
set { count = value; }
}
索引可以使我们像访问数组元素那样访问一个对象的元素。例如,对如下Java代码:
//String secretWord = ...
//char[] guessedSoFar = ...
for (int i = 0; i < secretWord.length(); i++) {
guessedSoFar[i] = secretWord.charAt(i);
}
相应的C#代码是:
//string secretWord = ...
//char[] guessedSoFar = ...
for (int i = 0; i < secretWord.Length; i++) {
guessedSoFar[i] = secretWord[i];
}
由于Java没有类似C#的Indexer,所以只能通过charAt
方法来访问字符串中的字符,而C#的string由于有Indexer,所以可以像访问数组元素那样访问字符串中的字符secretWord[i]
。
定义一个Indexer类似这样:
//char[] content = ...
public char this[int i] {
get { return content[i]; }
set { content[i] = value; }
}
最后,Java要求一个文件最多只能定义一个公共类(public class),并且文件名要与公共类的名字相同,C#则没有这样的限制。
嵌套类(Nested Class)
C#的嵌套类与Java不大相同。如下Java代码:
class MyGuessingStrategy implements GuessingStrategy {
private static class WordSet extends AbstractCollection<String> {
//...
}
}
相当于如下C#代码:
public class MyGuessingStrategy : IGuessingStrategy {
private class WordSet : ICollection<string> {
//...
}
}
注意上面的C#代码:嵌套类WordSet之前并无static
修饰符,因为C#嵌套类都相当于Java的static
修饰的嵌套类。
此外,C#并没有Java那样的非static
嵌套类,你需要在嵌套类里保存一个外部类引用,如果用得到的话。因此,如下Java代码:
private static class WordSet extends AbstractCollection<String> {
private class WordIterator implements Iterator<String> {
private Iterator<String> it = WordSet.this.words.iterator();
//...
}
}
对应的C#代码是:
private class WordSet : ICollection<string> {
private class WordIterator : IEnumerator<string> {
private readonly IEnumerator<string> it;
public WordIterator(WordSet outer) {
it = outer.words.GetEnumerator();
}
//...
}
}
另外需要注意的是,C#的类定义,不论是否嵌套,都可以用static
来修饰,但它的含义是:所修饰的类只含有static成员。
类型(Type)
Java的类型可分为primitive类型(包括int
、double
、char
、boolean
等共8种)与非primitive类型(所有从java.lang.Object
派生而来的类,包括java.lang.Object
本身)。primitive类型都是所谓的“值类型(Value Type)”,非primitive类型则是“引用类型(Reference Type)”。C#的类型系统与Java有相似之处但又很不一样,我们可以通过代码来比较:
以下Java代码
private Set<Character> correctlyGuessedLetters = new HashSet<Character>();
对应的C#代码是
private ISet<char> correctlyGuessedLetters = new HashSet<char>();
要在Java代码中定义一个char
Set,我们必须用Set<Character>
而不是Set<char>
。这是因为Java的范型(Generic)只支持引用类型,而char
是值类型,所以必须用char
对应的引用类型Character
。Java的primitive类型都是值类型、都有对应的引用类型,又称为“包装类(Wrapper)”2。
C#与此不同:它没有所谓的包装类,因为它的范型既支持引用类型也支持值类型。
此外,C#还允许用户通过struct
关键字定义自己的“值类型”——实际上,它的primitive类型,如int
、char
等,都是系统定义的struct。而且不论“值类型”还是“引用类型”,都派生自System.Object
。因此C#的类型系统比Java更加统一。
C#显式接口实现(Explicit Interface Implementation)
这是C#专有的一个特性,用以解决来自两个不同接口的方法签名冲突的问题(当你需要在C#中实现一个集合(ICollection)时就要用到,见下文)。举例来说:
interface IA {
void Foo();
}
interface IB {
int Foo();
}
class C : IA, IB {
//? Foo()
}
C同时实现了IA和IB,但这样实现是不合法的:
class C : IA, IB {
public void Foo() {}
public int Foo() { return 1; }
}
你不能重载一个方法——仅仅是返回值不同。这时就需要显示接口实现:
class C : IA, IB {
public void Foo() {}
int IB.Foo() { return 1; }
}
注意IB.Foo()
之前不可以有public
修饰符。
你可以在C或者IA上调用void Foo()
,或者在IB上调用int Foo()
,像这样:
C c = new C();
c.Foo(); //Call void Foo()
IA a = c;
a.Foo(); //Call void Foo()
IB b = c;
b.Foo(); //Call int Foo()
C#扩展方法(Extension)
这也是C#的一个专有特性。当你在MSDN上查看某个类型的文档时,可能会发现其下有大量并不属于该类型定义本身的“Extension Methods”,比如对ICollection,Aggregate
、First
……都是扩展方法。
我们的C#算法实现也在ICollection接口上定义了一个扩展方法AddAll
:
public static class MyExtension {
public static void AddAll<T>(this ICollection<T> to, ICollection<T> from) {
foreach (T e in from) {
to.Add(e);
}
}
};
AddAll
方法的第一个参数带有修饰符this
,表示这个方法可以扩展到ICollection<T>
类型的对象上。然后就可以这么使用它:
public ISet<char> AllGuessedLetters {
get {
ISet<char> guessed = new HashSet<char>();
guessed.AddAll(correctlyGuessedLetters);
guessed.AddAll(incorrectlyGuessedLetters);
return guessed;
}
}
就好像这个方法是定义在ICollection上一样。
需要注意的是:扩展方法不可以在被扩展对象的构造函数里使用,所以:
class WordSet {
public WordSet(string pattern, ISet<char> guessedLetters, ICollection<string> words) {
//...
//这里不可以使用 this.AddAll(words)
MyExtension.AddAll(this, words);
//...
}
}
集合与迭代(Collection and Iteration)
这里的集合是指实现了特定接口的对象。对Java而言它是java.util.Collection:
public interface Collection<E> extends Iterable<E>
对C#是System.Collections.Generic.ICollection:
public interface ICollection<T> : IEnumerable<T>, IEnumerable
集合是一个很重要的概念,我们日常所用的List、Set和Map3都是集合。集合有一些共同的操作/方法,围绕集合还有一些通用的算法,如排序。
迭代及其接口
集合最基本的操作是迭代,或者说枚举其中的元素。对此Java和C#都有相应的语言支持:
Java使用增强的for
语句来迭代:
//Set<String> dict = ...
List<String> words = new ArrayList<String>();
for (String word : dict) {
if (word.length() == len) {
words.add(word);
}
}
C#使用foreach
:
//ISet<string> dict = ...
IList<string> words = new List<string>();
foreach (string word in dict) {
if (word.Length == len) {
words.Add(word);
}
}
Java和C#集合对象可以迭代的关键是它们都实现了某种可迭代接口:
对于Java它是java.lang.Iterable:
public interface Iterable<T> {
Iterator<T> iterator();
//...
}
对于C#则是System.Collections.Generic.IEnumerable:
public interface IEnumerable<out T> : IEnumerable {
IEnumerator<T> GetEnumerator();
}
可迭代接口的关键是返回一个迭代器:
Java Iterator:
public interface Iterator<E> {
boolean hasNext();
E next();
//...
}
C# IEnumerator:
public interface IEnumerator<out T> : IDisposable, IEnumerator {
bool MoveNext();
T Current { get; }
//...
}
实现一个集合
当我们要实现一个自己的集合时,就必须实现可迭代接口,以及其他一些必要的集合方法。
在这方面,Java要更容易一些:Java提供了一个抽象类java.util.AbstractCollection,只要实现了iterator
和size
方法就能实现一个集合,例如:
private static class WordSet extends AbstractCollection<String> {
private class WordIterator implements Iterator<String> {
//...
}
@Override
public boolean add(String word) {
//...
}
@Override
public int size() {
return words.size();
}
@Override
public Iterator<String> iterator() {
return new WordIterator();
}
}
WordSet
还实现了add
方法用以向集合中添加元素。
在C#里实现一个集合就比较麻烦:C#没有提供类似的抽象类,所以你要从头开始实现ICollection的每个方法(好在它们也不算多),另外,由于ICollection<T>
同时继承了IEnumerable<T>
与IEnumerable
,而这两个接口中存在签名相同的方法,所以你必须要用到上文提到的显示接口实现(Explicit Interface Implementation)。对于IEnumerator<T>
来说也有同样的问题。
最终,与上面Java代码对应的C#代码像这样:
private class WordSet : ICollection<string> {
private class WordIterator : IEnumerator<string> {
public string Current => it.Current;
//Explicit Interface Implementation
object IEnumerator.Current => it.Current;
//...
}
public int Count => words.Count;
public void Add(string word) {
//...
}
public IEnumerator<string> GetEnumerator() {
return new WordIterator(this);
}
//Explicit Interface Implementation
IEnumerator IEnumerable.GetEnumerator() {
return GetEnumerator();
}
//Other methods in ICollection...
}
集合上的操作/方法
Java和C#都对集合提供了很多通用算法,如排序、查找等。Java的java.util.Collections
类用静态方法提供了这些支持。C#通过上文提到的扩展方法提供支持,比如对于ICollection<T>
,这些扩展多来自于System.Linq.Enumerable
。
总结
我们的对比只限于一些基本方面,也并非要一分伯仲,实际上总的来说,这两种编程语言在伯仲之间:
- Java提供的语言设施要精简一些、C#更加丰富,比如属性、索引、扩展方法等。这对用户来说是好事,因为使用起来方便。但坏的方面是这增加了语言的复杂度以及你掌握它的成本。
- C#提供了某些Java无法提供的解决方案,比如显式接口实现。这很好,但同上一条一样,这种方案不是没有成本的,它使语言更加复杂,然而它解决的问题可能并不是日常会遇到的,不过由于它在.Net集合框架的实现中被大量使用,所以就人为地成了日常会遇到的问题。
- C#的类型系统更加统一。这对于使用范型来说明显比Java更加统一、方便。
-
这不是没有原因的:Java作者曾经想给它命名为
C++++--
,意思是给C++加上一些好东西、去掉一些不好的东西。C#用一个“#”表示4个“+”号,意思是C++的加强版。 ↩ -
读者可能会想:既然
Character
的使用范围比char
更广泛,那不如在所有用到char
的地方都使用Character
就好了。但这样并不好,因为Java虚拟机操作primitive类型比它的wrapper更高效。另外,在primitive类型和它的wrapper之间进行转换还涉及到所谓boxing/unboxing的问题。 ↩ -
Java的
Map
比较特殊:它虽然也是集合但并不从Collection
接口派生。它的entrySet
方法返回一个“键-值”Set
可以进行for
迭代。 ↩