与 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 会自动转换 GString
和 String
。要小心那些形参为 Object
的 Java API,需要检查它们的实际类型。
9. 字符串和字符字面量
在 Groovy 中,由单引号所创建的字面量属于 String
类型对象,而双引号创建的字面量则可能是 String
或 GString
对象,具体分类由字面量中是否有插值来决定。
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
。从 Float
或 Double
转换时,Groovy 使用 Number.doubleValue()
来构建 BigInteger
和 BigDecimal
,否则将使用 toString()
来构建。其他转换则有 java.lang.Number
所定义的行为。
13. 额外的关键字
Groovy 的关键字要比 Java 中的多一些。不要使用以下这些关键字来定义变量名等名称:
as
def
in
trait
14. default
必须位于 swith / case 结构的结尾
default
必须位于 swith / case 结构的结尾。在 Java 中,它可以放在 swith / case 结构中的任何位置,但在 Groovy 中,它更像是一个 else 子句,而非一个默认的 case 子句。