该文章从Java的C#程序员的角度概述了Java和C#编程语言之间的差别。这种比较不是百科全书式的,而是强调了一些潜在的麻烦或其余显着的基本点。在适当的地方注明了Java SE 5-11主要版本中引入的新功能。html
因为两种语言都与它们各自的运行时紧密相关,所以,我还将介绍Java虚拟机(JVM)与.NET Framework以及基本库类之间的任何相关差别。我没有介绍用于网络,序列化,GUI,XML等的库框架。在这些方面,两个平台的功能大体相同,可是实现方式却大不相同。java
C#7及更高版本 — C#6发布后,Microsoft将快速发展的语言移至Github,而且显然再也不费心编写适当的规范。某些新功能是直接从Java复制的(例如,数字分隔符),可是一般能够假定C#7+功能在Java中不可用。此页面的其他部分仅涵盖版本6以前的C#。git
进一步阅读
有关如何编译和运行Java程序的快速入门,请参阅编译Java代码。程序员
Oracle的JDK 11文档包括全部参考资料,包括Java语言和VM规范,Java Platform SE 11 API规范以及Java教程。有关Java语言和库元素的全面性能分析,请参见Mikhail Vorontsov的Java Performance Tuning Guide。github
最好的印刷介绍是Horstmann的Core Java和Bloch的Effective Java,以及Horstmann的《不耐烦的Core Java》做为快速概述。请参阅Java书籍以获取更多建议。Andrei Rinea的教程系列“ .NET Developers的Beginning Java”更详细地介绍了选定的主题。web
文章内容
1.关键字
Java和C#使用很是类似的语法,并提供类似的关键字集,它们均源自C / C ++。在这里,我将简要列出如下各节中未说起的一些值得注意的区别。算法
assert
等效于C#Debug.Assert
调用。该断言设备须要一种新的语言关键字,由于Java提供没有其余办法的Elid有条件方法调用。有关捕获断言失败的详细信息,请参见异常。(Java SE 1.4)class
在类型名称后面typeof
用做“类文字”时,它等效于C#(示例)。final
等价于具备const
编译时常量的原始或字符串变量上的C#;readonly
其余领域的C#和C#sealed
的类和方法。Java保留const
但不使用它。instanceof
与C#等效,is
用于检查对象的运行时类型。没有等效的C#as
,所以在类型检查成功后,您必须始终使用显式强制转换。native
extern
与声明外部C函数的C#等效。- C#
string
丢失。您必须始终使用大写的库类型String
。 synchronized
等效于C#MethodImplOptions.Synchronized
做为方法属性,并等效于方法中lock
代码块周围的C#。transient
等同于C#NonSerializableAttribute
,可免除字段的序列化。var
像C#中同样声明隐式类型的局部变量。(Java SE 10,但请参见下文)Object... args
(三个句点)等效于C#params
,而且还从全部列出的参数隐式建立一个数组。(Java SE 5)
SE 10以前的Java版本缺少,var
但提供了lambda表达式(Java SE 8)和泛型类型参数的类型推断,包括泛型构造函数的菱形符号。不要var
与通用类型推断结合使用,不然省略的类型变量可能被推断为Object
!有关更多信息,请参见Stuart W. Marks的样式指南。express
缺乏关键字
Java彻底缺乏如下C#关键字和功能:编程
#if/#define/#pragma
和条件编译。解决方法包括动态类加载和外部预处理器或其余构建工具。#region
块没有等效项,可是Java IDE固然容许语法折叠。async/await
用于异步回调和yield break/return
枚举器。您必须手工编写所需的状态机。dynamic
在运行时进行动态键入。Java SE 7提供了invokedynamic JVM指令,但未在Java中公开它。event
以及事件的隐式代码生成。您必须手动编写整个事件基础结构,如从C#到Java:事件中所示(但请参见下文)。fixed/sizeof/stackalloc/unsafe
和指针操做。解决方法包括native
方法和特定于平台的库。get/set
用于相似字段的属性。使用具备相应前缀的传统方法语法(由JavaBeans模式标准化)。operator/explicit/implicit
和运算符重载,包括自定义索引器和转换运算符。请参阅有关字符串运算符的特殊说明。partial
类和方法。每一个类和非抽象方法都在一个文件中彻底定义。ref/out
和按引用致电。经过引用传递方法参数的惟一方法是将它们包装在另外一个对象中,例如一个元素数组。- 命名和可选方法参数。一样,您必须使用对象包装器经过名称设置参数或提供默认参数值。
- 经过名称设置成员的对象初始化程序,以及经过索引设置元素的索引初始化程序。您可使用难看的双括号初始化习惯来模拟它们。
- C#6中的如下任何新功能:空条件(
?.
)和nameof
运算符,表达式函数,异常过滤器(catch…when
)和字符串插值($
)。
做为编写本身的事件基础结构的替代方法,请考虑使用JavaFX包javafx.beans及其子包中定义的“可观察”对象。此类对象使您能够附加在值更改时获得通知的侦听器-一般是事件的预期用途。(注意:从Java SE 11开始,JavaFX已成为单独的下载,如今能够在此处下载。)api
Java没有等效于LINQ关键字。从Java SE 8开始,lambda表达式和流库将LINQ的基于方法的化身复制到对象,并完成了惰性和/或并行评估。Java中相似LINQ的查询的第三方库包括iciql,Jinq,jOOQ和QueryDSL。
2.原语
Java和C#具备等效的原始类型集(int
等),但如下状况除外:
- Java仅具备带符号的数字类型,包括
byte
。缺乏全部未签名的变体。Java SE 8将未签名的操做添加为库方法。 - C#
decimal
与Java类BigDecimal类似,后者没有相应的原语。 - 全部Java原语都有等效的库类。可是,这些不是 C#中的同义词,而是盒装版本。这是由于Java 在其类层次结构中不支持值类型。
- 出于一样的缘由,Java没有基元的可为空的变体。只需使用等效的库类-它们都是引用类型,所以能够为空。
- 除了数字值的十进制,十六进制和八进制文字外,Java SE 7还添加了二进制文字和下划线文字。
请注意,C#和Java中的基本等效类之间的区别。在C#中,int
而且System.Integer
是同义词:二者都表明未装箱的原始值类型。您须要进行(Object)
强制转换以将引用明确地包装在这些值周围。
可是在Java中,只有int
一个未装箱的原始值,而java.lang.Integer表明强类型的装箱版本!C#程序员必须注意不要互换使用Java原语和相应的类名。有关更多详细信息,请参见自动装箱。(Java SE 5)
Java会根据须要在分配,数学运算或方法调用的上下文中自动将原始值从其强类型包装对象中解包。显式转换到原始类型从更通常的类拆箱时只须要(Number
,Object
)。
3.算术
Java缺乏C#checked/unchecked
来切换对算术表达式的溢出检查。相反,积分溢出会像C / C ++同样默默地截断位,而浮点溢出会产生负无穷或正无穷大。浮点0/0产生NaN(不是数字)。只有被零除的整数会抛出ArithmeticException
。Java SE 8 …Exact
向java.lang.Math添加了各类用于算术运算和类型转换的方法,这些方法老是在溢出时引起异常。
Java提供了strictfp
可应用于类,接口和方法的修饰符。它强制将全部浮点运算的中间结果截断为IEEE 754大小,以在各个平台上实现可重复的结果。库类java.lang.StrictMath还基于fdlibm定义了一组具备可移植行为的标准函数。
4.控制流程
Java保留goto
但未定义它。可是,语句标签确实存在,而且发生了奇怪的变化,break
而且continue
获得了加强,能够接受它们。您只能跳出本地块,但能够break
在任何块上操做-不只是循环。这使得Java break
几乎彻底等同于C#goto
。
Java switch
对(装箱或未装箱的)原语和枚举进行操做,而且因为Java SE 7也对字符串进行操做。您不须要case
像C#中那样用枚举类型限定枚举值。Java容许落空,从一个case
到下一个,就像C / C ++。使用编译器选项-Xlint:fallthrough
来警告缺乏的break
语句。没有等效于C#goto
定位case
标签的方法。
5.数组
与.NET中同样,Java数组是带有自动索引检查的专用引用类型。与.NET不一样,Java直接仅支持一个数组维。多维数组是经过嵌套一维数组来模拟的。可是,在初始化过程当中指定全部尺寸时,全部必需的嵌套数组都将隐式分配。例如,new int[3][6]
分配外部数组和六个整数的全部三个嵌套数组,而无需重复new
语句。能够不指定任何数量的最右边维,以便之后手动建立一个不规则的数组。
辅助类Arrays提供了许多与数组相关的功能:比较,转换,复制,填充,哈希码生成,分区,排序和搜索,以及做为实例方法Array.toString
没法实现的人类可读字符串输出。Java SE 8添加了几种parallel…
在可能的状况下执行多线程操做的方法。
6.琴弦
与C#中同样,Java字符串是UTF-16 代码单元的不可变序列,每一个序列适合一个16位char
原语。对于包含任何32位Unicode代码点(即,现实世界中的字符)的字符串,它们须要两个 UTF-16代码单元的替代对,则不能使用普通的char
索引器方法。而是使用各类辅助方法来对代码点创建索引,Java直接在String类上定义了这些方法。
Java SE 9引入了仅包含ISO-8859-1(Latin-1)字符的字符串的紧凑表示形式。这样的字符串每一个字符使用8位而不是16位。这是一种自动且纯粹是内部的优化,不会影响公共API。
运算符 —与C#不一样,Java不会==
对字符串的运算符进行特殊区分,所以只会测试引用是否相等。使用String.equals或String.equalsIgnoreCase测试内容是否相等。
Java +
对字符串链接运算符进行特殊处理(JLS§15.18.1)。烦人的是,这会执行相似于JavaScript的全部类型的自动转换为String
。一旦String
找到一个操做数,就将左侧的任何现有中间和与右侧的其他全部单个操做数转换String
并链接在一块儿。与C#中同样,逐段字符串链接也可能效率不高。使用专用的StringBuilder类可得到更好的性能。
7.班级
Java缺乏C#static
类。若要建立仅包含静态实用程序方法的类,请使用定义私有默认构造函数的老式方法。Java确实具备static
类修饰符,但仅适用于嵌套类且具备很是不一样的语义。
类对象包含一些简单但有用的用于任何类型对象的帮助程序方法,包括为多个对象生成哈希码以及各类空安全操做。
构造 —构造函数链的用法this(…)
与C#和super(…)
基类中的用法相同,但这些调用显示为构造函数主体中的第一行,而不是在大括号以前。
Java没有静态构造函数,可是提供了静态和实例变量的匿名初始化程序块。多个初始化程序块是能够接受的,而且将按它们在任何构造函数运行以前出现的顺序执行。静态初始化程序块在首次加载该类时执行。
销毁 -Java支持在垃圾回收器销毁对象以前运行的终结器,可是这些终结器的名称很合理,finalize
而不是C#误导性的C ++析构函数语法。终结器的行为是复杂且有问题的,所以一般应首选try/finally
清理。
Java不只提供能够随时收集的弱引用(如C#),还提供仅响应内存压力而收集的软引用。
8.继承
基类称为超类,所以用关键字super
而不是C#进行引用base
。声明派生类时,Java extends
(用于超类)和Java (用于implements
接口)指定了相同的继承关系,C#为此使用简单的冒号。
C#virtual
彻底丢失,由于除非明确声明,不然全部 Java方法都是虚拟的final
。几乎没有性能损失,由于JVM Hotspot优化器与至关愚蠢的.NET CLR优化器不一样,能够在运行时未检测到覆盖时动态内联虚拟方法。
Java SE 5添加了协变返回类型以支持其类型擦除泛型。协方差是经过编译器生成的bridge方法实现的,所以请注意子类的版本控制问题。
9.接口
像C#同样,Java支持单类继承和多接口继承。接口名称没有I
.NET 前缀,也没有以任何其余方式与类名称区分开。
Java不支持C#扩展方法来将实现从外部附加到接口(或类)上,可是它容许在接口内实现。Java 接口可能包含常量,即public static final
字段。字段值能够是复杂的表达式,在第一次加载接口时会对其进行评估。
Java SE 8 在接口上添加了静态方法和默认方法,以及Java SE 9 私有方法。在没有常规类实现的状况下使用默认方法。这消除了对抽象默认实现类的需求,而且还容许扩展接口而不会破坏现有客户端。
Java不支持C#显式接口实现来从公共视图中隐藏接口要求的方法。这多是一件好事,由于在涉及多个类时,显式接口实现的访问语义众所周知很容易出错。
10.嵌套类
使用static
修饰符定义嵌套类,其行为与C#中的行为相同。没有该修饰符的嵌套类是Java的特殊内部类。它们也可能以专用的类名或匿名方式出如今方法内。
内部类 -非静态嵌套类是内部类,它们对建立它们的外部类实例进行隐式引用,相似于this
实例方法的隐式引用。您还能够用于关联特定的外部类实例。内部类能够访问其外部类的全部私有字段和方法,能够选择使用前缀来消除歧义。嵌套类上的修饰符可防止这种隐式关联。outerObj.new InnerClass()
OuterClass.this
static
本地类 -内部类可能在方法中显示为本地类。除了隐式关联的外部类实例的全部私有成员以外,局部类还能够访问声明方法中范围内的全部局部变量,只要它们有效final
。
匿名类 -本地类能够声明为一次性实例,并带有用于指定超类或接口的初始化器表达式。编译器在内部生成一个具备隐藏名称的类,该名称扩展了超类或实现了接口。这种作法被称为匿名类。
在Java SE 8以前,匿名类与lambda表达式等效,尽管它可能包含大多数普通类成员,但它们的功能更为强大。禁止使用构造函数– 而是使用初始化程序块。(双括号初始化习惯用法可能会滥用此功能。)
Java的功能编程版本最终依赖于匿名类。所以,仅定义单个方法的接口称为功能接口,而实现它们的匿名类称为功能对象。
11. Lambda表达式
Java SE 8添加了lambda表达式做为功能对象的替代,在语法上更加简洁,内部实现也更快。语法与C#相同,只是用->
代替而不是=>
做为功能箭头。若是不存在,则推断参数类型。
Java在java.util.function
其余地方预约义了基本的功能接口。不幸的是,因为Java的类型擦除泛型以及缺乏值类型,所以预约义类型比.NET的委托库丑陋且不全面。埃德温·达洛佐(Edwin Dalorzo)解释了细节,并警告与已检查异常的可能冲突。
因为lambda表达式在语义上等效于匿名类,所以它们隐式地键入为调用者须要的任何接口,例如Comparator <T>。与C#delegate
类型同样,您可使用函数接口类型将lambda表达式存储在变量中。此外,lambda表达式能够访问外部做用域中任何有效的局部变量,这些局部变量实际上final
使人惊讶地包括for-each循环变量。
方法参考 —除了在须要函数对象的地方定义lambda表达式以外,您还能够提供对任何现有静态方法或实例方法的方法参考。使用双冒号(::
)将类名或实例名与方法名分开。此语法还能够将超类方法引用为,将构造函数引用为。经过将类型化的数组构造函数传递给通用方法,能够建立任何所需类型的数组。super::method
ClassName::new
int[]::new
尽管很是方便,但对实例方法的方法引用与等效的lambda表达式的求值方法有所不一样,这可能致使使人惊讶的行为。有关示例,请参见Java方法参考评估。
12.枚举
Java SE 5引入了类型安全的枚举,以代替松散的整数常量。与C#enum
类型不一样,Java枚举是成熟的引用类型。每一个枚举常量表明一个该类型的命名实例。用户不能建立除枚举实例以外的任何新实例,以确保声明的常量列表是最终的。这种独特的实现有两个重要的后果:
- Java枚举变量能够为null,默认为null。这意味着您没必要定义单独的“无值”常量,可是若是确实须要有效的枚举值,则必须执行空检查。
- Java枚举类型支持任意字段,方法和构造函数。这使您能够将任意数据和功能与每一个枚举值相关联,而无需外部帮助程序类。(在这种状况下,每一个枚举值都是一个匿名子类实例。)
两个专门的集合EnumMap和EnumSet提供带有或不带有关联数据的枚举值的高性能子集。EnumSet
在将C#枚举与[Flags]
属性一块儿使用时使用。内部实现其实是相同的,即位向量。
13.值类型
Java的一个重大缺陷是缺乏用户定义的值类型。在还没有肯定的将来版本中发布时,Valvala 项目应提供具备泛型支持的.NET样式值类型- 有关更多详细信息,请参见建议值状态和最小值类型。目前,Java提供的惟一值类型是其原语,它们彻底位于类层次结构以外。本节简要描述了对语义,性能和泛型的影响。
语义 -值类型具备两个重要的语义属性:它们不能为null(即具备“无值”),而且它们的所有内容在每次分配时复制,从而使全部副本在未来的变异方面彼此独立。当前,对于Java中的用户定义类型而言,第一个属性是没法实现的,只能经过频繁的空检查来近似。
使人惊讶的是,第二个属性可有可无,由于值类型不管如何都应该是不变的,由于微软发现了难题。默认状况下,.NET中的值类型是可变的,而且因为隐式复制操做而不会致使模糊错误的出现。如今,标准建议是使全部值类型都是不可变的,而且对于相似值的Java类(例如)也是如此BigDecimal
。可是,一旦对象成为不可变的,则突变的理论效果就可有可无了。
性能 -值类型将其内容直接存储在堆栈上或嵌入在其余对象中,而无需引用或其余元数据。这意味着它们须要的内存要少得多,前提是内容不比元数据大不少。并且,减小了垃圾收集器的工做量,而且不须要解引用步骤来访问内容。
Oracle的Server VM很是擅长优化 C#将实现为值类型的小对象,所以计算性能没有太大差别。可是,额外的元数据不可避免地会膨胀大量的小对象。您须要复杂的包装器类来解决此问题,例如,参见Java中的紧凑堆外结构/堆栈。
泛型 —如Valhalla:Goals项目中所述,基元不是类这一事实意味着它们不能做为泛型类型实参出现。您必须改用等效的类包装器(例如Integer
用于int
),从而致使昂贵的装箱操做。避免这种状况的惟一方法是专用于原始类型参数的通用类的硬编码变体。Java库充斥着此类专业知识。在此以前,没有更好的解决方案,直到Valvala项目提供将原语集成到类层次结构中的值类型。
14.封装和模块
Java包在很大程度上等效于C#名称空间,但有一些重要区别。从Java SE 9开始,模块提供了其余功能,用于依赖性检查和访问控制。
储存格式
Java类加载器须要一个目录结构,该目录结构复制声明的包结构。幸运的是,Java编译器能够自动建立该结构(-d .
)。此外,每一个源文件只能包含一个公共类,而且必须具备该类的名称,包括确切的大小写。
这些限制带来了意想不到的好处:Java编译器具备集成的“ mini-make”功能。因为全部目标文件的位置和名称均已明确规定,所以编译器能够自动检查哪些文件须要更新,而仅从新编译这些文件。
为了分发,一般将已编译的Java类文件的整个目录结构以及元数据和任何所需的资源放置在Java归档(JAR)中。从技术上讲,这只是一个普通的ZIP文件。.jar
可执行文件和库的扩展名都相同。前者在内部被标记为具备主要阶级。
与.NET程序集不一样,JAR文件是彻底可选的,没有语义意义。全部访问控制都是经过程序包和(在更大程度上)模块声明来实现的。在这方面,Java程序包和模块结合了.NET名称空间和程序集的功能。
配套
Java 包是组织类的基本方法。它们对于大型项目(如JDK自己)的表达能力还不够高,这致使开发了Java SE 9的新模块系统。可是,非模块化软件包仍然受到支持,而且对于较小的应用程序就足够了。
声明 — Java package
语句等效于C#namespace
块,但隐式适用于整个源文件。这意味着您不能在单个源文件中混合软件包,可是与C#格式相比,它确实消除了一种毫无心义的缩进。
Java import
等同于C#using
进行名称空间导入,但始终引用单个类。使用.*
导入包中的全部类。该形式import static
等效于using static
(C#6),而且容许使用无限制的静态类成员(Java SE 5)。可是,没有类名别名。
存储 -包含程序包源代码的目录可能包含package-info.java
仅用于文档说明的可选文件。在非模块化应用程序中,同一包的目录树能够在不一样的子项目中屡次出现。全部可见事件的内容都将合并。
可见性 —类,方法和字段的默承认见性是程序包内部的。这大体等效于C#,internal
但指的是声明的包(C#名称空间),而不是物理部署单元(JAR文件或.NET程序集)。所以,外部代码只需声明本身是同一包的一部分,就能够访问部署单元中全部默承认见的对象。若是您想防止这种状况,则必须明确地密封 JAR文件,不然请使用模块(Java SE 9)。
没有为默认的公开程度没有专门的关键字,因此它暗示,若是没有public
,private
也不protected
是存在的。C#程序员必须特别注意将私有字段标记为private
避免此默认值!并且,Java protected
等效于C#internal protected
,即对派生类和同一包中的全部类可见。您不能将可见性仅限于子类。
最后,Java软件包没有“朋友”访问(InternalsVisibleTo
属性)的概念,该概念能够提升对特定其余软件包或类的可见性。任何其余软件包都应可见的软件包成员必须为public
或protected
。
模组
Java SE 9引入了将任意数量的软件包与显式依赖项和可见性声明结合在一块儿的模块。从技术上讲,如今全部代码都在模块中运行,可是为了向后兼容,将任何非模块化代码都视为依赖于全部现有模块并导出其全部包的模块。
Oracle当前不提供有关模块的简明文档。您能够浏览Mark Reinhold的连接声明,查阅Java语言规范的第7章,或为不耐烦的人购买Cay Horstmann的Core Java 9。如下概述并不详尽。
声明和存储 —每一个模块对应一个具备(任意)模块名称的目录,其中包含module-info.java
全部包含的软件包的文件和子目录树。这些包被照常声明。全部模块声明都位于中module-info.java
,使用仅在此处有效的特殊Java关键字。
依赖性 - requires
声明当前模块所需的任何模块。(可选)transitive
将必需模块设为任何使用当前模块的人的隐含要求。不须要的模块对于当前模块不可用,即便它们存在于模块路径中也是如此。
可见性 - exports
声明全部导出的软件包以供使用,并opens
声明全部能够对外反射的软件包。(可选)exports/opens
能够将可见性限制为给定的命名模块列表。其余模块看不到任何不可见的软件包。所以,public
未导出程序包的internal
成员等效于C#成员。
尽管模块的名称可能与软件包的名称相同,可是应用程序中的全部模块名称和全部可见的软件包名称都必须是惟一的。所以,不可能扩充在另外一个模块中声明的包,从而解决Java包的奇怪漏洞。
15.例外
Java因其检查的异常而臭名昭著,即,若是方法抛出但未捕获它们,则必须在throws
子句中指定异常类型。长期以来,基于程序员的心理(检查编译器错误经过吞下异常使编译器错误静音,这比不处理异常更糟糕)和组件交互的缘由,人们一直在争论检查异常的价值。
例如,无心义的throws
子句可能在最坏的状况下扩散开,或者在不适当的位置处理异常以阻止这种扩散。在设计C#时,Anders Hejlsberg著名地拒绝了检查异常。一些程序员只是经过将检查异常包装在未检查异常中而彻底避免了它们,尽管Oracle不喜欢这种作法。
可是,从概念上讲,检查异常很是简单:检查全部异常,除非源自Error
(严重的内部错误)或RuntimeException
(一般是编程错误)。一般的怀疑是在正常操做期间可能会发生的I / O错误,必须进行相应的处理。
不然,Java异常处理与C#很是类似。Java SE 7 在一个块中添加了多种异常类型catch
,而且该try
版本复制了C#using
。在尝试与-资源声明依赖于对(Auto)Closeable
接口,就像C#using
依赖IDisposable
。Java SE 9也容许try-with-resources 有效地使用final变量。
的断言错误 -对全部运行时错误Java的基类是不 Exception
做为.NET而是Throwable
从双方Exception
和Error
派生。不幸的是AssertionError
,因为assert
失败Error
而抛出的Java 是一个而不是一个Exception
。所以,若是您但愿处理断言错误以及异常(例如在后台线程上),则必须捕获Throwable
而不是Exception
。有关详细信息和连接,请参见捕获Java断言错误。
跳转和finally
—与C#中同样,在finally
子句中引起的异常会丢弃关联try
块中先前引起的异常。不像C#,即禁止跳下finally
,只是返回从Java finally
条款也放弃之前的全部例外!这样作的缘由使人惊讶的行为是全部跳转语句(break
,continue
,return
)列为在一样的意义为“忽然结束” throw
。该finally
条款的忽然结束丢弃该try
块之前的忽然结束。
尽管实际上不太可能发生,但更奇怪的结果是,跳出finally
会覆盖return
关联try
块中的普通语句。有关示例和更多信息,请参见最终跳出Java。启用编译器选项-Xlint:finally
以检查此陷阱。
16.泛型
在Microsoft将它们添加到.NET 2的两年前, Java SE 5引入了泛型。尽管两个版本的源代码看起来都类似,可是底层实现却大不相同。为了确保最大的向后兼容性,Sun选择了类型擦除,该类型擦除在运行时消除类型变量,并用非泛型等效项替换全部泛型类型。这确实容许与遗留代码(包括预编译的字节码)进行无缝互操做,但要付出新开发的巨大代价。
C#泛型简单,高效且几乎是万无一失的。Java泛型相似于C ++模板,它们倾向于生成难以理解的编译器错误,可是甚至不支持将非装箱的原语做为类型参数!若是要使用Java有效地调整大小的整数集合,则不能使用List<T>
etc的任何实现,由于这将对全部元素形成浪费的装箱。
相反,您必须定义本身的非通用集合,int
并将其硬编码为元素类型,就像在普通C或.NET 1的糟糕年代同样。(固然,您也可使用多个第三方库之一)。)泛型中的基元计划做为Valhalla项目的一部分–参见上面的“ 值类型”和Ivan St. Ivanov的文章系列“ 泛型中的基元”(第2 部分,第3部分)。
我没有试图解释Java和C#泛型之间的复杂区别,而是将您引到上面引用的资源以及Angelika Langer极其全面的Java泛型FAQ中。在本节的其他部分,我将仅介绍一些值得注意的要点。
构造 — Java缺乏C#new
约束,可是仍然容许使用类文字技巧来实例化泛型类型参数。Class<T> c
为T
方法提供所需类型参数的类文字,而后在方法内使用c.newInstance()
来建立type的新实例T
。
从Java 8开始,您还可使用对方法的引用,这些方法是与lambda表达式一块儿引入的,并在该部分中进行了介绍。
静态字段 -静态字段在通用类的全部类型实例化之间共享。这是类型擦除的结果,该类型擦除将全部不一样的实例折叠为一个运行时类。C#进行相反的操做,并为每一个泛型类型实例化的全部静态字段分配新的存储。
Java不容许静态字段和方法使用任何泛型类型变量。类型擦除将Object
在共享的运行时类上使用(或一些更特定的非泛型类型)产生单个字段或方法。因为类型擦除,对于来自不一样类型实例化的不一样类型实参,只有实例字段和方法才是类型安全的。
类型界限 — 通用类型变量的可选界限(JLS§4.4)与C#类似,但语法不一样。边界由一种主要类型(类或接口)和零个或多个接口边界组成,并附加&
。例如,<T extends C & I>
等效于C#<T> where T: C, I
。这确保了实际类型T
是一些亚型C
也实现了接口I
,其C
自己并无实现。有趣的是,Java还容许类型转换表达式中的接口边界(JLS§15.16)。
无效(Void) -正如没法将基元指定为泛型类型参数同样,也没法指定关键字void
。将Void类用于实现类不使用的通用接口的任何类型参数。
通配符 -从未引用的任何泛型类型参数均可以简单地指定为?
所谓的通配符。通配符也能够用或限制。这容许像C#这样的协变和矛盾,但不只限于接口。要引用通配类型参数,请使用声明命名类型参数的单独方法捕获它。extends
super
in/out
有一个与通配符有关的巧妙技巧。若是容器包含带有通配符的某些常规元素类型,例如TableView <S> .getColumns返回的集合,则能够将具备不一样具体类型的通配符实例放入同一容器中。在C#中,不一样的具体类型参数产生不兼容的类,这是不可能的。
17.馆藏
在Java集合框架(教程)大大优于其至关于.NET设计。集合变量一般是从丰富的接口层次结构中键入的。它们几乎与其实现类同样强大,所以后者仅用于实例化。所以,大多数集合算法均可以在具备适当语义的任何符合框架的集合上工做。这包括各类可组合的包装方法,例如动态子范围和只读视图。
接口方法和具体实现的某些组合可能效果不佳,例如索引链表。Java倾向于公开一个可能缓慢的操做,而不是根本不公开该操做,这一般是.NET限制性更强的收集接口的状况。
迭代器 -Java容许在迭代其元素时对集合进行变异,但只能经过当前的迭代器进行。Java还具备专门的ListIterator,能够返回其元素索引。使用集合迭代器时,.NET既不容许进行突变也不容许进行索引检索。
Java SE 5添加了一个等效于C#语句的for-each循环foreach
,但没有专用关键字。此循环未公开Java集合迭代器的变异和索引检索功能。与C#中同样,数组上的for-each循环是特殊状况,以免建立迭代器对象。
流 -Java SE 8添加了流和管道,这些流和管道连接方法要求累积操做,例如基于方法的C#LINQ版本。能够从常规集合建立流,也能够从生成器函数或外部文件建立流。管道仅在须要时获取新元素,而且能够顺序或并行处理它们。Lambda表达式用于自定义管道操做。最后,终端操做将结果转换为另外一个常规Java对象或集合。
18.注释
Java SE 5引入了与.NET属性大体等效的注释。注释使用任意元数据标记程序元素,以供之后由库方法或编程工具提取。除了语法上的差别外,还有一些C#开发人员值得注意的要点:
- 注释不能更改带注释的程序元素的语义。特别是,它们没法像
[Conditional("DEBUG")]
.NET断言那样彻底抑制方法调用。 - @FunctionalInterface验证接口仅包含单个方法,所以能够经过lambda表达式或方法引用来实现。
- @Override替换
override
了Java语言中莫名其妙缺乏的C#。 - @SuppressWarnings和特定形式@SafeVarargs至关于C#
#pragma warning
。一般将它们与Java的类型擦除泛型一块儿使用。
Java SE 8 除了类型声明外,还容许注释类型用法。可是,您须要外部工具才能今后类注释中受益。
19.评论
与C#同样,Java 为类和方法上的代码注释定义了一种标准格式,这些注释能够提取为格式化的HTML页面。与C#不一样,JDK附带的Javadoc处理器直接执行输出格式化,所以您不须要外部格式化程序,例如NDoc或Sandcastle。
尽管Javadoc缺少编译器检查的方式来引用注释文本中的参数,但功能类似。语法有很大不一样,而且更加简洁,由于Javadoc主要依靠隐式格式和紧凑@
标签。HTML标签仅在不@
存在适当标签的状况下使用。
若是您须要将大量的C#XML注释转换为Javadoc格式,则应查看个人注释转换器,它能够为您完成大部分机械翻译。
摘要 -默认状况下,Javadoc注释的第一句话会自动视为其摘要。Java SE 10引入了标签{@summary … }
以显式定义摘要,该摘要等效于<summary>
C#XML注释的元素。