返回首页 Groovy 入门

Getting started

与 Java 的区别

Groovy 试图尽可能地让 Java 开发者快速适应。在设计 Groovy 时,我们努力不让用户感到惊讶,即遵循“最小惊讶”原则,特别是针对那些此前有 Java 开发背景的 Groovy 初学者。

下面讲讲 Groovy 与 Java 的主要不同点。

1. 默认导入

下面这些包和类都是默认导入的,也就是说,不用再显式地使用 import 语句了:

  • java.io.*
  • java.lang.*
  • java.math.BigDecimal
  • java.math.BigInteger
  • java.net.*
  • java.util.*
  • groovy.lang.*
  • groovy.util.*

2. 多重方法

在 Groovy 中,调用的方法将在运行时被选择。这种机制被称为运行时分派或多重方法(multi-methods),是根据运行时实参(argument)的类型来选择方法。Java 采用的是相反的策略:编译时根据声明的类型来选择方法。

下面的 Java 代码可以用 Java 和 Groovy 来编译,但两种编译结果截然不同:

int method(String arg) {
    return 1;
}
int method(Object arg) {
    return 2;
}
Object o = "Object";
int result = method(o);

用 Java 编译的结果如下:

assertEquals(2, result);

用 Groovy 编译的结果则为:

assertEquals(1, result);

产生差异的原因在于,Java 使用静态数据类型,o 被声明为 Object 对象,而 Groovy 会在运行时实际调用方法时进行选择。因为调用的是 String 类型的对象,所以自然调用 String 版本的方法。

3. 数组初始化表达式

在 Groovy 中,{...} 语句块是留给闭包(Closure)使用的,这意味着你不能使用以下这种格式来创建数组:

int[] array = { 1, 2, 3}

正确的方式是这样的:

int[] array = [1,2,3]

4. 包范围可见性

在 Groovy 中,如果某个字段缺失了修饰符,并不会导致像在 Java 中那样形成包的私有字段:

class Person {
    String name
}  

相反,它会用来创建一个属性property),也就是一个私有字段private field),以及一个关联的 getter 和一个关联的 setter

在 Groovy 中创建包私有字段,可以通过标注 @PackageScope 来实现。

class Person {
    @PackageScope String name
}

5. ARM 语句块

ARM(Automatic Resource Management,自动资源管理)语句块从 Java 7 开始引入,但 Groovy 并不支持。相反,Groovy 提供多种基于闭包的方法,效果相同但却合乎习惯。例如:

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();
}

可以写成下面这样的代码:

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

或者,如果你想让它更接近于 Java 的惯用形式,可以这样写:

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

6. 内部类

Groovy 中的匿名内部类和内嵌类的实现跟 Java 是一样的,但你不应拿 Java 语言规范来考量它,应对差异情况保持冷静与宽容。已完成的实现看起来有点类似 groovy.lang.Closure 类的实现。一方面,这使得访问私有字段和方法可能会比较麻烦,但另一方面,本地变量也不必非要设置成 final 了。

6.1 静态内部类

下面是一个静态内部类的例子:

class A {
    static class B {}
}

new A.B()

静态内部类是最受支持的,所以如果你确实需要一个内部类,应该首先考虑静态内部类。

6.2 匿名内部类

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 中,你可以这样做:

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),如下所示:

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 表达式

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

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

Java 8 的 lambda 几乎可以认为是匿名内部函数。Groovy 不支持这种格式,而采用闭包来实现。

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

8. GString

由于双引号所包括起来的字符串字面量会被解释为 GString 值(即 “Groovy 字符串”的简称),所以如果当某个类中的 String 字面量含有美元字符($)时,那么利用 Groovy 和 Java 编译器进行编译时,Groovy 很可能就会编译失败,或者产生与 Java 编译所不同的结果。

通常,如果某个 API 声明了形参的类型,Groovy 会自动转换 GStringString。要小心那些形参为 Object 的 Java API,需要检查它们的实际类型。

9. 字符串和字符字面量

在 Groovy 中,由单引号所创建的字面量属于 String 类型对象,而双引号创建的字面量则可能是 StringGString 对象,具体分类由字面量中是否有插值来决定。

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

当把一个包含单个字符的 String 对象赋予一个 char 类型变量时,Groovy 会自动将该对象转换为 char 类型。在调用带有 char 类型实参的方法时,我们需要显式地转换参数值,或者确认参数值已经预先转换过了。

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 模式的转换则出现异常错误,从而失败。

// 对于仅包含单个字符的字符串,两种转换模式是等效的   
assert ((char) "c").class==Character
assert ("c" as char).class==Character

// 对于包含多个字符的字符串来说,两种模式的结果并不相同 
try {
  ((char) 'cx') == 'c'
  assert false: 'will fail - not castable'
} catch(GroovyCastException e) {
}
assert ('cx' as char) == 'c'
assert 'cx'.asType(char) == 'c'

10.

11. == 的行为差异

在 Java 中,== 代表基本数据类型的相同,或对象引用的等价性。在 Groovy 中,== 的含义变成了 a.compareTo(b)==0,不过这要当且仅当 == 两边的对象都实现了 Comparable 接口时才能实现,否则 == 就等同于 a.equals(b)。而要想在 Groovy 中检查对象间的引用等价性,则需使用 is,比如:a.is(b)

12. 转换

Java 能够自动执行一些扩宽转换和缩减转换,关于转换的概念参见这里

表1 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 大大扩展这些转换。

表2 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 Truth。从数值转换为字符是将 Number.intvalue() 转换为 char。从 FloatDouble 转换时,Groovy 使用 Number.doubleValue() 来构建 BigIntegerBigDecimal,否则将使用 toString() 来构建。其他转换则有 java.lang.Number 所定义的行为。

13. 额外的关键字

Groovy 的关键字要比 Java 中的多一些。不要使用以下这些关键字来定义变量名等名称:

  • as
  • def
  • in
  • trait

14. default 必须位于 swith / case 结构的结尾

default 必须位于 swith / case 结构的结尾。在 Java 中,它可以放在 swith / case 结构中的任何位置,但在 Groovy 中,它更像是一个 else 子句,而非一个默认的 case 子句。

上一篇: 安装 Groovy 下一篇: Groovy 开发工具...