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
2
3
4
5
6
7
8
int method(String arg) {
return 1;
}
int method(Object arg) {
return 2;
}
Object o = "Object";
int result = method(o);

在 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
2
3
class Person {
String name
}

在 Groovy 中这样会创建出一个属性(Property),也就是一个 private 域和对应的 Getter 和 Setter 方法。

通过为域添加上 @PackageScope 注解即可将其声明为包内可见:

1
2
3
class Person {
@PackageScope String name
}

5 ARM 块

Groovy 不支持 Java7 的自动资源管理(Automatic Resource Management, ARM)代码块,而是提供了各种不同的利用了闭包的方法,使得我们可以使用更简洁的写法来达成同样的效果。例如:

1
2
3
4
5
6
7
8
9
10
11
Path file = Paths.get("/path/to/file");
Charset charset = Charset.forName("UTF-8");
try (BufferedReader reader = Files.newBufferedReader(file, charset)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}

} catch (IOException e) {
e.printStackTrace();
}

可被写作:

1
2
3
new File('/path/to/file').eachLine('UTF-8') {
println it
}

或者,如果你想让它看起来更像 Java 的话,也可以这样写:

1
2
3
4
5
new File('/path/to/file').withReader('UTF-8') { reader ->
reader.eachLine {
println it
}
}

6 内部类

Groovy 的匿名内部类和嵌套类在某种程度上以 Java 为指导,但你不需要再翻阅 Java 语言规范并苦想二者之间的差异。实际的实现实际上与 groovy.lang.Closure 很接近,只是还多了一点其他的不同,例如无法访问私有的域或方法,但局部变量则不需要被声明为 final 了。

6.1 静态内部类

如下为静态内部类的案例:

1
2
3
4
5
class A {
static class B {}
}

new A.B()

实际上,Groovy 对静态内部类的支持是最好的,因此如果你确实需要一个内部类的话,你应该将其声明为静态的。

6.2 匿名内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

CountDownLatch called = new CountDownLatch(1)

Timer timer = new Timer()
timer.schedule(new TimerTask() {
void run() {
called.countDown()
}
}, 0)

assert called.await(10, TimeUnit.SECONDS)

6.3 创建非静态内部类的实例

在 Java 中,你可以这样:

1
2
3
4
5
6
7
8
9
public class Y {
public class X {}
public X foo() {
return new X();
}
public static X createX(Y y) {
return y.new X();
}
}

Groovy 并不支持像 y.new X() 这样的语法。你应该像如下代码那样,写成 new X(y)

1
2
3
4
5
6
7
8
9
public class Y {
public class X {}
public X foo() {
return new X()
}
public static X createX(Y y) {
return new X(y)
}
}

值得注意的是,Groovy 允许你在调用只有一个参数的方法时不给出任何实参。如此一来参数值会被设置为 null。对构造器的调用同样遵循此规则。因此你有可能会写成 new X() 而不是 new X(this)。由于这样做在某种情况下也有可能是合理的,因此我们还没有找出一个很好的办法来避免这样的问题。

7 Lambda 表达式

Java8 支持 Lambda 表达式和方法引用:

1
2
Runnable run = () -> System.out.println("Run");
list.forEach(System.out::println);

Java8 的 Lambda 表达式在某种程度上可以被看作是匿名内部类。Groovy 不支持这样的语法,但支持闭包:

1
2
Runnable run = { println 'run' }
list.each { println it } // or list.each(this.&println)

8 GString

由于带双引号的字符串字面量会被解析为 GString 对象,如果一个类包含一个 String 字面量其中包含了美金符号,Groovy 可能会无法编译或是给出与 Java 编译器所给出的大相径庭的代码。

尽管 Groovy 能够根据 API 声明的参数类型来对 GString
对象和 String 对象家进行自动转换,你仍然需要注意那些将参数类型声明为 Object 但在方法体内对实参类型进行判断的 Java API。

9 StringCharacter 字面量

在 Groovy 中,带单引号的字符串字面量被用作 String 对象的创建,而带双引号的字符串字面量则会创建出 GStringString 对象,取决于字面两种是否包含插值占位符。

1
2
3
assert 'c'.getClass()==String
assert "c".getClass()==String
assert "c${1}".getClass() in GString

只有当赋值给一个类型为 char 的变量时,Groovy 才会自动地将一个只包含一个字符的 String 转换为 char 类型。当你想调用一个参数类型为 char 的方法时,你需要显式地对类型进行转换或者预先进行类型转换。

1
2
3
4
5
6
7
8
9
char a='a'
assert Character.digit(a, 16)==10 : 'But Groovy does boxing'
assert Character.digit((char) 'a', 16)==10

try {
assert Character.digit('a', 16)==10
assert false: 'Need explicit cast'
} catch(MissingMethodException e) {
}

Groovy 支持两种不同的类型转换语法,而当转换包含多个字符的字符串至 char 时,两种语法会有不同的表现。Groovy 风格的类型转换会更为智能,只以字符串的第一个字符作为转换结果,而 C 风格的强制类型转换则会直接抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
// for single char strings, both are the same
assert ((char) "c").class==Character
assert ("c" as char).class==Character

// for multi char strings they are not
try {
((char) 'cx') == 'c'
assert false: 'will fail - not castable'
} catch(GroovyCastException e) {
}
assert ('cx' as char) == 'c'
assert 'cx'.asType(char) == 'c'

10 基本数据类型和包装类

由于在 Groovy 中所有东西都是对象,Groovy 会对对基本数据类型的引用进行自动包装。鉴于此,Groovy 不会像 Java 那样让类型扩充享有比装箱更高的优先级。例如:

1
2
3
4
5
6
7
8
9
10
int i
m(i)

void m(long l) { // 注1
println "in m(long)"
}

void m(Integer i) { // 注2
println "in m(Integer)"
}
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 的强制转换;当转换至 BigIntegerBigDecimal 时,如果源类型为 FloatDouble,Groovy 会使用 Number.doubleValue() 来构建结果,否则则使用 toString() 的 结果来构建。其他类型转换的行为均如 java.lang.Number 类所定义。

13 新增的关键词

Groovy 比起 Java 新增了如下几个关键词。不要将它们用作变量名等标识符:

  • as
  • def
  • in
  • trait
作者

Robert Peng

发布于

2018-04-24

更新于

2021-05-17

许可协议

评论