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 中编译,但其行为会有所不同:
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.数组初始化
在 Java 中,数组初始值设定项采用以下两种形式之一:
// Java 数组初始化
int[] array = {1, 2, 3}; // 短 语法
int[] array2 = new int[] {4, 5, 6}; // 长 语法
在 Groovy 中,该{ … }
块是为闭包保留的。这意味着您不能使用 Java 的数组初始值设定项简写语法来创建数组字面量。您可以像这样借用 Groovy 的文字列表表示法:
int[] array = [1, 2, 3]
对于 Groovy 3+,您可以选择使用 Java 的数组初始化器长语法:
def array2 = new int[] {1, 2, 3}
// Groovy 3.0+ 支持java数组的长语法初始化
4.包范围可见性
在 Groovy 中,省略字段上的修饰符不会导致像 Java 中那样的包私有字段:
class Person {
String name
}
相反,它用于创建属性,即私有字段、关联的_getter_
和关联的 _setter_
。
可以通过使用以下注释来创建包私有字段@PackageScope
:
class Person {
@PackageScope String name
}
5.ARM块
Java 7 引入了 ARM(自动资源管理)块,如下所示:
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();
}
Groovy 3+ 支持此类块。但是,Groovy 提供了各种依赖闭包的方法,它们具有相同的效果,但更惯用。例如:
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.内部类
:::warning 匿名内部类和嵌套类的实现与Java 密切相关,但存在一些差异,例如从此类类中访问的局部变量不必是final。我们groovy.lang.Closure 在生成内部类字节码时使用了一些实现细节。 :::
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();
}
}
在 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会在GString
和String
之间自动转换,但要注意接受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
。
当从Float
或Double
转换时,Groovy使用Number.doubleValue()
构造BigInteger
和BigDecimal
,否则它使用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
示例会被认为是糟糕的风格。
其他文档可用于关键字。