1.默认导入

所有这些包和类都是默认导入的,即您不必使用显式import语句来使用它们:

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

2.多方法

在 Groovy 中,将被调用的方法是在运行时选择的。这称为运行时调度或多方法。这意味着将在运行时根据参数的类型选择方法。在 Java 中,情况正好相反:方法是在编译时根据声明的类型选择的。

以下代码编写为 Java 代码,可以在 Java 和 Groovy 中编译,但其行为会有所不同:

  1. int method(String arg) {
  2. return 1;
  3. }
  4. int method(Object arg) {
  5. return 2;
  6. }
  7. Object o = "Object";
  8. int result = method(o);

在 Java 中,您将拥有:

  1. assertEquals(2, result);

而在 Groovy 中:

  1. assertEquals(1, result);

这是因为Java将使用静态信息类型,即o被声明为Object,而Groovy将在实际调用方法时在运行时进行选择。因为它是用String调用的,所以调用String版本。

3.数组初始化

在 Java 中,数组初始值设定项采用以下两种形式之一:

  1. // Java 数组初始化
  2. int[] array = {1, 2, 3}; // 短 语法
  3. int[] array2 = new int[] {4, 5, 6}; // 长 语法

在 Groovy 中,该{ … }块是为闭包保留的。这意味着您不能使用 Java 的数组初始值设定项简写语法来创建数组字面量。您可以像这样借用 Groovy 的文字列表表示法:

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

对于 Groovy 3+,您可以选择使用 Java 的数组初始化器长语法:

  1. def array2 = new int[] {1, 2, 3}
  2. // Groovy 3.0+ 支持java数组的长语法初始化

4.包范围可见性

在 Groovy 中,省略字段上的修饰符不会导致像 Java 中那样的包私有字段:

  1. class Person {
  2. String name
  3. }

相反,它用于创建属性,即私有字段、关联的_getter_和关联的 _setter_
可以通过使用以下注释来创建包私有字段@PackageScope

  1. class Person {
  2. @PackageScope String name
  3. }

5.ARM块

Java 7 引入了 ARM(自动资源管理)块,如下所示:

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

Groovy 3+ 支持此类块。但是,Groovy 提供了各种依赖闭包的方法,它们具有相同的效果,但更惯用。例如:

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

或者,如果您想要更接近 Java 的版本:

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

6.内部类

:::warning 匿名内部类和嵌套类的实现与Java 密切相关,但存在一些差异,例如从此类类中访问的局部变量不必是final。我们groovy.lang.Closure 在生成内部类字节码时使用了一些实现细节。 :::

6.1.静态内部类

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

  1. class A {
  2. static class B {}
  3. }
  4. new A.B()

静态内部类的使用是最受支持的一种。如果你绝对需要一个内部类,你应该把它变成一个静态的。

6.2.匿名内部类

  1. import java.util.concurrent.CountDownLatch
  2. import java.util.concurrent.TimeUnit
  3. CountDownLatch called = new CountDownLatch(1)
  4. Timer timer = new Timer()
  5. timer.schedule(new TimerTask() {
  6. void run() {
  7. called.countDown()
  8. }
  9. }, 0)
  10. 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();
    }
}

在 3.0.0 之前,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)
    }
}

:::warning 但请注意,Groovy 支持使用一个参数调用方法而不提供参数。然后该参数的值为 null。基本上相同的规则适用于调用构造函数。例如,您可能会编写 new X() 而不是 new X(this)。由于这也可能是常规方法,因此我们尚未找到防止此问题的好方法。 :::

:::info Groovy 3.0.0 支持用于创建非静态内部类实例的 Java 样式语法。 :::

7.Lambda 表达式和方法引用运算符

Java 8+ 支持 lambda 表达式和方法引用运算符 ( ::):

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

Groovy 3 及更高版本也在 Parrot 解析器中支持这些。在早期版本的 Groovy 中,您应该改用闭包:

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

8.GStrings

由于双引号字符串文字被解释为GString值,如果使用Groovy和Java编译器编译包含一个$的字符串文字的类,Groovy可能会失败并出现编译错误,或者生成稍微不同的代码。
虽然通常情况下,如果API声明参数的类型,Groovy会在GStringString之间自动转换,但要注意接受Object参数然后检查实际类型的Java API。

9.字符串和字符文字

Groovy 中的单引号文字用于String,而双引号结果为 String 或者 GString,具体取决于文字中是否存在插值。

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

仅当分配给类型为 char 的变量时,Groovy 才会自动将单个字符String转换为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.==行为

在Java中,==表示基本类型的相等或对象的标识。
在Groovy中,==表示所有位置都相等。对于非基础类型,当计算可比较对象的相等性时,它转换为a.compareTo(b)==0,否则转换为a.equals(b)

要检查标识(参考等式),请使用is方法:a.is(b)
在Groovy 3中,还可以使用===运算符(或求反版本):a===b(或c!==d)。

11.基础类和包装器

在纯粹的面向对象语言中,一切都是对象。Java 的立场是,原始类型,例如 int、boolean 和 double,使用非常频繁,值得特殊处理。基础类型可以有效地存储和操作,但不能在可以使用对象的所有上下文中使用。幸运的是,当它们作为参数传递或用作返回类型时,Java 自动装箱和拆箱原语:

jshell> class Main {
...>   float f1 = 1.0f;
...>   Float f2 = 2.0f;
...>   float add(Float a1, float a2) { return a1 + a2; }
...>   Float calc() { return add(f1, f2); } 
...> }
|  created class Main

jshell> new Main().calc()
$2 ==> 3.0

:::info 该add方法需要包装器然后是原始类型参数,但是我们提供的参数是原始然后包装器类型。同样, from 的返回类型add是原始类型,但我们需要包装类型。 :::

Groovy 也是如此:

groovy:000> class Main {
groovy:001>   float f1 = 1.0f
groovy:002>   Float f2 = 2.0f
groovy:003>   float add(Float a1, float a2) { a1 + a2 }
groovy:004>   Float calc() { add(f1, f2) }
groovy:005> }
===> true
groovy:000> new Main().calc()
===> 3.0

Groovy 也支持原语和对象类型,但是,它在推动 OO 纯度方面走得更远;它努力将一切都视为对象。任何原始类型变量或字段都可以被视为对象,并且会根据需要自动包装。虽然原始类型可能会在幕后使用,但它们的使用应该尽可能与普通对象使用没有区别,并且它们将根据需要被装箱/拆箱。

这是一个使用 Java 尝试(对于 Java 不正确)取消引用原语的小示例float:

jshell> class Main {
   ...>     public float z1 = 0.0f;
   ...> }
|  created class Main

jshell> new Main().z1.equals(1.0f)
|  Error:
|  float cannot be dereferenced
|  new Main().z1.equals(1.0f)
|  ^------------------^

使用 Groovy 的相同示例编译并运行成功:

groovy:000> class Main {
groovy:001>     float z1 = 0.0f
groovy:002> }
===> true
groovy:000> new Main().z1.equals(1.0f)
===> false

由于 Groovy 对取消/装箱的额外使用,它不遵循 Java 扩大优先于装箱的行为。这是一个使用示例int

int i
m(i)

// 这是 Java 会调用的方法,因为扩展优先于拆箱。
void m(long l) {           
    println "in m(long)"
}

// 这是 Groovy 实际调用的方法,因为所有原始引用都使用它们的包装类。
void m(Integer i) {        
    println "in m(Integer)"
}

11.1.数字基础类型优化

由于 Groovy 在更多地方转换为包装类,您可能想知道它是否会为数字表达式生成效率较低的字节码。Groovy 有一组高度优化的用于进行数学计算的类。使用 @CompileStatic 时,仅涉及原语的表达式使用 Java 将使用的相同字节码。

11.2.正/负零边缘情况

原语和包装类的 Java 浮点/双精度操作都遵循 IEEE 754 标准,但有一个有趣的边缘情况涉及正零和负零。该标准支持区分这两种情况,虽然在许多场景中程序员可能不关心差异,但在某些数学或数据科学场景中,重要的是要注意区分。
对于原语,Java在比较具有“正零和负零被认为相等”的属性的值时映射到特殊的 字节码指令

jshell> float f1 = 0.0f
f1 ==> 0.0

jshell> float f2 = -0.0f
f2 ==> -0.0

jshell> f1 == f2
$3 ==> true

对于包装类,例如java.base/java.lang.Float#equals(java.lang.Object),结果是false相同的情况。

jshell> Float f1 = 0.0f
f1 ==> 0.0

jshell> Float f2 = -0.0f
f2 ==> -0.0

jshell> f1.equals(f2)
$3 ==> false

Groovy 一方面试图密切关注 Java 行为,但另一方面在更多地方自动在原语和包装等效项之间切换。为避免混淆,我们建议遵循以下准则:

  • 如果您希望区分正零和负零,请直接使用 equals 方法或使用==.
  • 如果您希望忽略正零和负零之间的差异,请直接使用 equalsIgnoreZeroSign 方法或在使用==.

这些准则在以下示例中进行了说明:

float f1 = 0.0f
float f2 = -0.0f
Float f3 = 0.0f
Float f4 = -0.0f

assert f1 == f2
assert (Float) f1 != (Float) f2

// 回想一下,对于非基础类型,==映射到.equals()
assert f3 != f4         
assert (float) f3 == (float) f4

assert !f1.equals(f2)
assert !f3.equals(f4)

assert f1.equalsIgnoreZeroSign(f2)
assert f3.equalsIgnoreZeroSign(f4)

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 不可以进行的转换。

表 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 相同的关键字,而 Groovy 3 及更高版本也具有与Java 相同的保留var类型。此外,Groovy 有以下关键字:

  • as
  • def
  • in
  • trait
  • it// 在闭包内

Groovy 没有 Java 严格,因为它允许一些关键字出现在 Java 中非法的地方。
例如以下是有效的:var var = [def: 1, as: 2, in: 3, trait: 4]
尽管如此,即使编译器可能支持,也不鼓励您在可能导致混淆的地方使用上述关键字。
特别是,避免将它们用于变量、方法和类名,因此我们前面的var var示例会被认为是糟糕的风格。
其他文档可用于关键字