Groovy 教程 - 与 Java 的差异
这是一篇译文,读者可前往 Groovy Getting Started - Differences with Java 阅读原文。
Groovy 语言在设计时便考虑到要尽可能让语言本身令 Java 程序员感到自然。如此,我们在设计 Groovy 时则尽可能让其少出现出人意料的地方,尤其是对于那些有着 Java 背景的开发者。
在这篇文章中我们将列举几处 Java 和 Groovy 的显著差异。
1 默认引入
如下的这些包和类都会被默认引入 —— 也就是说,你不需要显式的 import
语句即可使用它们:
java.io.*
java.lang.*
java.math.BigDecimal
java.math.BigInteger
java.net.*
java.util.*
groovy.lang.*
groovy.util.*
2 多方法
在 Groovy 中,方法会在运行时被选择并调用。这种机制被称为运行时分发(Runtime Dispatch)或多方法(Multi-methods)。这意味着具体被调用的方法会在运行时根据实参的类型被挑选。在 Java 中则是截然相反:具体被调用的方法会在编译期根据实参的声明类型被挑选。
如下 Java 代码可以同时在 Java 环境和 Groovy 环境中编译运行,但却会有不同的行为:
1 | int method(String arg) { |
在 Java 中,你会有:
1 | assertEquals(2, result); |
而在 Groovy 中则会有:
1 | assertEquals(1, result); |
这是因为 Java 会利用静态信息类型(变量 o
被声明为 Object
)来挑选被调用的方法,而 Groovy 则会在方法被确实调用的运行时才进行选择。由于调用时所使用的实参是一个 String
,那么 String
版本的方法就被调用了。
3 数组初始化语句
在 Groovy 中,{...}
块被保留用作定义闭包。也就是说,你不能像如下语句这样来创建数组字面量:
1 | int[] array = { 1, 2, 3 }; |
你需要这样:
1 | int[] array = [1, 2, 3] |
4 包可见性
在 Groovy 中,不给出任何修饰符并不会使得一个类的域像 Java 那样仅在该包内可见:
1 | class Person { |
在 Groovy 中这样会创建出一个属性(Property),也就是一个 private
域和对应的 Getter 和 Setter 方法。
通过为域添加上 @PackageScope
注解即可将其声明为包内可见:
1 | class Person { |
5 ARM 块
Groovy 不支持 Java7 的自动资源管理(Automatic Resource Management, ARM)代码块,而是提供了各种不同的利用了闭包的方法,使得我们可以使用更简洁的写法来达成同样的效果。例如:
1 | Path file = Paths.get("/path/to/file"); |
可被写作:
1 | new File('/path/to/file').eachLine('UTF-8') { |
或者,如果你想让它看起来更像 Java 的话,也可以这样写:
1 | new File('/path/to/file').withReader('UTF-8') { reader -> |
6 内部类
Groovy 的匿名内部类和嵌套类在某种程度上以 Java 为指导,但你不需要再翻阅 Java 语言规范并苦想二者之间的差异。实际的实现实际上与 groovy.lang.Closure
很接近,只是还多了一点其他的不同,例如无法访问私有的域或方法,但局部变量则不需要被声明为 final
了。
6.1 静态内部类
如下为静态内部类的案例:
1 | class A { |
实际上,Groovy 对静态内部类的支持是最好的,因此如果你确实需要一个内部类的话,你应该将其声明为静态的。
6.2 匿名内部类
1 | import java.util.concurrent.CountDownLatch |
6.3 创建非静态内部类的实例
在 Java 中,你可以这样:
1 | public class Y { |
Groovy 并不支持像 y.new X()
这样的语法。你应该像如下代码那样,写成 new X(y)
:
1 | public class Y { |
值得注意的是,Groovy 允许你在调用只有一个参数的方法时不给出任何实参。如此一来参数值会被设置为 null
。对构造器的调用同样遵循此规则。因此你有可能会写成 new X()
而不是 new X(this)
。由于这样做在某种情况下也有可能是合理的,因此我们还没有找出一个很好的办法来避免这样的问题。
7 Lambda 表达式
Java8 支持 Lambda 表达式和方法引用:
1 | Runnable run = () -> System.out.println("Run"); |
Java8 的 Lambda 表达式在某种程度上可以被看作是匿名内部类。Groovy 不支持这样的语法,但支持闭包:
1 | Runnable run = { println 'run' } |
8 GString
由于带双引号的字符串字面量会被解析为 GString
对象,如果一个类包含一个 String
字面量其中包含了美金符号,Groovy 可能会无法编译或是给出与 Java 编译器所给出的大相径庭的代码。
尽管 Groovy 能够根据 API 声明的参数类型来对 GString
对象和 String
对象家进行自动转换,你仍然需要注意那些将参数类型声明为 Object
但在方法体内对实参类型进行判断的 Java API。
9 String
和 Character
字面量
在 Groovy 中,带单引号的字符串字面量被用作 String
对象的创建,而带双引号的字符串字面量则会创建出 GString
或 String
对象,取决于字面两种是否包含插值占位符。
1 | assert 'c'.getClass()==String |
只有当赋值给一个类型为 char
的变量时,Groovy 才会自动地将一个只包含一个字符的 String
转换为 char
类型。当你想调用一个参数类型为 char
的方法时,你需要显式地对类型进行转换或者预先进行类型转换。
1 | char a='a' |
Groovy 支持两种不同的类型转换语法,而当转换包含多个字符的字符串至 char
时,两种语法会有不同的表现。Groovy 风格的类型转换会更为智能,只以字符串的第一个字符作为转换结果,而 C 风格的强制类型转换则会直接抛出异常。
1 | // for single char strings, both are the same |
10 基本数据类型和包装类
由于在 Groovy 中所有东西都是对象,Groovy 会对对基本数据类型的引用进行自动包装。鉴于此,Groovy 不会像 Java 那样让类型扩充享有比装箱更高的优先级。例如:
1 | int i |
1 | 如果是 Java 的话就会调用这个方法,因为类型扩充比装拆箱享有更高的优先级 |
2 | Groovy 则会调用这个方法,因为所有对基本数据类型变量的引用的类型实际上都是为其对应的包装类 |
11 ==
的行为
在 Java 中,==
用于检验基本数据类型的相等性和引用的一致性。而在 Groovy 中,对于 Comparable
类,==
会被理解为 a.compareTo(b) == 0
,否则理解为 a.equals(b)
。要检测引用的一致性,需要这样写:a.is(b)
。
12 转换
Java 会自动进行类型扩充或类型收窄的转换。
转换至 | ||||||||
---|---|---|---|---|---|---|---|---|
转换自 | boolean |
byte |
short |
char |
int |
long |
float |
double |
boolean |
- | N | N | N | N | N | N | N |
byte |
N | - | Y | C | Y | Y | Y | Y |
short |
N | C | - | C | Y | Y | Y | Y |
char |
N | C | C | - | Y | Y | Y | Y |
int |
N | C | C | C | - | Y | T | Y |
long |
N | C | C | C | C | - | T | T |
float |
N | C | C | C | C | C | - | Y |
double |
N | C | C | C | C | C | C | - |
* 'Y'
即指 Java 可以自动执行该转换,'C'
即指 Java 可在显式声明了强制类型转换时执行该转换,'T'
即指 Java 可执行该转换但会导致有效数据被删节,'N'
即指 Java 无法执行该转换。
Groovy 则大大扩充了这些转换规则。
转换至 | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
转换自 | boolean |
Boolean |
byte |
Byte |
short |
Short |
char |
Character |
int |
Integer |
long |
Long |
BigInteger |
float |
Float |
double |
Double |
BigDecimal |
boolean |
- | B | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N |
Boolean |
B | - | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N |
byte |
T | T | - | B | Y | Y | Y | D | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
Byte |
T | T | B | - | Y | Y | Y | D | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
short |
T | T | D | D | - | B | Y | D | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
Short |
T | T | D | T | B | - | Y | D | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
char |
T | T | Y | D | Y | D | - | D | Y | D | Y | D | D | Y | D | Y | D | D |
Character |
T | T | D | D | D | D | D | - | D | D | D | D | D | D | D | D | D | D |
int |
T | T | D | D | D | D | Y | D | - | B | Y | Y | Y | Y | Y | Y | Y | Y |
Integer |
T | T | D | D | D | D | Y | D | B | - | Y | Y | Y | Y | Y | Y | Y | Y |
long |
T | T | D | D | D | D | Y | D | D | D | - | B | Y | T | T | T | T | Y |
Long |
T | T | D | D | D | T | Y | D | D | T | B | - | Y | T | T | T | T | Y |
BigInteger |
T | T | D | D | D | D | D | D | D | D | D | D | - | D | D | D | D | T |
float |
T | T | D | D | D | D | T | D | D | D | D | D | D | - | B | Y | Y | Y |
Float |
T | T | D | T | D | T | T | D | D | T | D | T | D | B | - | Y | Y | Y |
double |
T | T | D | D | D | D | T | D | D | D | D | D | D | D | D | - | B | Y |
Double |
T | T | D | T | D | T | T | D | D | T | D | T | D | D | T | B | - | Y |
BigDecimal |
T | T | D | D | D | D | D | D | D | D | D | D | D | T | D | T | D | - |
* 'Y'
即指 Groovy 可以执行该转换,'D'
即指 Groovy 进行动态编译或遇到显式类型转换语句时可执行该转换,'T'
即指 Groovy 可以执行该转换但有效数据会被删节,'B'
即指该转换为装箱/拆箱操作,'N'
即指 Groovy 不能执行该转换。
当转换至 boolean
/Boolean
时,Groovy 会使用 Groovy 真值;从数字到字符的转换实为从 Number.intvalue()
到 char
的强制转换;当转换至 BigInteger
或 BigDecimal
时,如果源类型为 Float
或 Double
,Groovy 会使用 Number.doubleValue()
来构建结果,否则则使用 toString()
的 结果来构建。其他类型转换的行为均如 java.lang.Number
类所定义。
13 新增的关键词
Groovy 比起 Java 新增了如下几个关键词。不要将它们用作变量名等标识符:
as
def
in
trait
Groovy 教程 - 与 Java 的差异