该文章从Java的C#程序员的角度概述了Java和C#编程语言之间的差异。这种比较不是百科全书式的,而是强调了一些潜在的麻烦或其他显着的基本点。在适当的地方注明了Java SE 5-11主要版本中引入的新功能。
由于两种语言都与它们各自的运行时紧密相关,因此,我还将介绍Java虚拟机(JVM)与.NET Framework以及基本库类之间的任何相关差异。我没有介绍用于网络,序列化,GUI,XML等的库框架。在这些方面,两个平台的功能大致相同,但是实现方式却大不相同。
C#7及更高版本 — C#6发布后,Microsoft将快速发展的语言移至Github,并且显然不再费心编写适当的规范。某些新功能是直接从Java复制的(例如,数字分隔符),但是通常可以假定C#7+功能在Java中不可用。此页面的其余部分仅涵盖版本6之前的C#。
进一步阅读
有关如何编译和运行Java程序的快速入门,请参阅编译Java代码。
Oracle的JDK 11文档包括所有参考资料,包括Java语言和VM规范,Java Platform SE 11 API规范以及Java教程。有关Java语言和库元素的全面性能分析,请参见Mikhail Vorontsov的Java Performance Tuning Guide。
最好的印刷介绍是Horstmann的Core Java和Bloch的Effective Java,以及Horstmann的《不耐烦的Core Java》作为快速概述。请参阅Java书籍以获取更多建议。Andrei Rinea的教程系列“ .NET Developers的Beginning Java”更详细地介绍了选定的主题。
文章内容
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的样式指南。
缺少关键字
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已成为单独的下载,现在可以在此处下载。)
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注释的元素。