Groovy语言探索
# 引言
# 1. 简介
Groovy是一种运行在Java平台上的动态语言,它兼容Java的语法,同时引入了许多新的语法和特性,使得编写Java程序变得更加简单和高效。
Groovy的语法更加接近自然语言,使得代码更易读,同时,其强大的动态特性使得编写复杂的逻辑变得更简单。
# 2. Groovy与Java的关系
Groovy和Java有着密切的关系,Groovy本身是运行在Java平台上的,其底层依赖于Java的运行环境。
因此,Groovy可以无缝地与Java进行互操作,你可以在Groovy中调用Java的库和类,同时,也可以在Java中调用Groovy的代码。
Groovy的语法是兼容Java的,你可以在Groovy的程序中使用纯Java的语法,这使得Java程序员可以很快地学习和使用Groovy。
同时,Groovy也引入了许多新的语法和特性,使得编写Java程序变得更加简单和高效。
在实际的开发中,Groovy和Java经常被一起使用,Groovy通常用于编写测试、构建脚本、以及其他需要快速编写和修改的代码,而Java则用于编写性能要求更高的核心代码。
通过这种方式,Groovy和Java可以完美地配合,提供了一种高效、强大的开发工具。
# Groovy与Java的区别
Groovy力图使得对Java开发人员来说尽可能自然。在设计Groovy时,尽力遵循最少惊讶原则,特别是针对那些来自Java背景的开发人员学习Groovy的情况。
下面列出了Java和Groovy之间的主要区别:
# 1. 默认导入
以下这些包和类默认被导入,即您无需使用显式的导入语句来使用它们:
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中,数组初始化有两种形式:
int[] array = {1, 2, 3}; // Java数组初始化的简写语法
int[] array2 = new int[] {4, 5, 6}; // Java数组初始化的长写语法
而在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(Automatic Resource Management)块(也称为try-with-resources)块,如下所示:
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. 内部类
对于匿名内部类和嵌套类的实现,Groovy遵循Java的方式,但也存在一些差异,例如,不必将局部变量声明为final。在生成内部类字节码时利用了一些在生成groovy.lang.Closure时使用的实现细节。
# a. 静态内部类
下面是静态内部类的示例:
class A {
static class B {}
}
new A.B()
对于静态内部类的使用是最受支持的一种。如果您确实需要内部类,请将其设置为静态内部类。
# b. 匿名内部类
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)
# c. 创建非静态内部类的实例
在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)
}
}
请注意,Groovy支持无需给出参数调用带有一个参数的方法。参数将会被赋予null值。基本上,调用构造函数也遵循相同的规则。
存在一个问题,即您可能会错误地编写new X()
而不是new X(this)
。由于这也可能是常规的方式,因此目前还没有找到一个很好的方法来解决这个问题。
Groovy 3.0.0支持Java风格的语法来创建非静态内部类的实例。
# 7. Lambda表达式和方法引用操作符
Java 8+支持Lambda表达式和方法引用操作符(::):
Runnable run = () -> System.out.println("Run"); // Java
list.forEach(System.out::println);
Groovy 3及以上版本也支持这些特性。在Groovy的早期版本中,您应该使用闭包代替:
Runnable run = { println 'run' }
list.each { println it } // 或者 list.each(this.&println)
# 8. GStrings
由于双引号字符串字面量被解释为GString值,因此如果一个包含美元符号的字符串字面量的类使用Groovy和Java编译器进行编译,可能会出现编译错误或产生细微差异的代码。
尽管通常情况下,如果API声明了参数的类型,Groovy将在GString和String之间自动进行类型转换,但要注意接受Object参数的Java API,然后检查实际类型。
# 9. 字符串和字符字面量
在Groovy中,使用单引号字面量表示字符串,而使用双引号字面量表示String或GString,取决于字面量中是否有插值。
assert 'c'.class == String
assert "c".class == String
assert "c${1}".class in GString
当将单个字符的字符串赋值给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. == 的行为
在Java中,== 表示原始类型的相等性或对象的身份。在Groovy中,== 在所有情况下表示相等性。对于非原始类型,它
会将其转换为a.compareTo(b) == 0
来评估Comparable对象的相等性,否则评估a.equals(b)
。
要检查身份(引用相等性),请使用is方法:a.is(b)
。从Groovy 3开始,您还可以使用===运算符(或否定版本):a === b
(或c !== d
)。
# 11. 原始类型和包装类
在纯面向对象的语言中,所有东西都是对象。Java认为原始类型(如int、boolean和double)在使用频率上很高,因此值得特殊对待。原始类型可以高效地存储和操作,但不能在所有可以使用对象的上下文中使用。幸运的是,当原始类型作为参数传递或作为返回类型使用时,Java会自动进行装箱和拆箱:
public class Main { // Java
float f1 = 1.0f;
Float f2 = 2.0f;
float add(Float a1, float a2) { return a1 + a2; }
Float calc() { return add(f1, f2); }
public static void main(String[] args) {
Float calcResult = new Main().calc();
System.out.println(calcResult); // => 3.0
}
}
add方法期望的是包装类,然后是原始类型的参数,但提供的参数是原始类型,然后是包装类。类似地,add的返回类型是原始类型,但需要包装类。
Groovy也是如此:
class Main {
float f1 = 1.0f
Float f2 = 2.0f
float add(Float a1, float a2) { a1 + a2 }
Float calc() { add(f1, f2) }
}
assert new Main().calc() == 3.0
Groovy也支持原始类型和对象类型,但它更进一步追求面向对象的纯洁性;它努力将所有东西都视为对象。任何原始类型的变量或字段都可以像对象一样对待,并且在需要时会自动进行包装。虽然在底层可能使用了原始类型,但在可能的情况下,它们的使用应该与正常的对象使用无异,并且会自动进行装箱/拆箱。
这是一个使用Java的示例,试图(对于Java来说是不正确的)取消引用一个原始的float类型:
public class Main { // Java
public float z1 = 0.0f;
public static void main(String[] args){
new Main().z1.equals(1.0f); // 不能编译,错误:float无法被取消引用
}
}
使用Groovy的相同示例可以成功编译和运行:
class Main {
float z1 = 0.0f
}
assert !(new Main().z1.equals(1.0f))
由于Groovy在使用上装箱/拆箱,它不遵循Java中优先选择装箱的行为。下面是一个使用int的示例:
int i
m(i)
void m(long l) {
println "in m(long)"
}
void m(Integer i) {
println "in m(Integer)"
}
这是Java将调用的方法,因为扩展优先于拆箱。
# a. 使用@CompileStatic进行数字原始类型优化
由于Groovy在更多的地方转换为包装类,您可能会想知道它是否会为数字表达式生成更低效的字节码。Groovy具有一组高度优化的用于执行数学计算的类。在使用@CompileStatic时,仅涉及原始类型的表达式使用与Java相同的字节码。
# b. 正零/负零边缘情况
Java的float/double操作(包括原始类型和包装类)遵循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
assert f3 != f4
assert (float) f3 == (float) f4
assert !f1.equals(f2)
assert !f3.equals(f4)
assert f1.equalsIgnoreZeroSign(f2)
assert f3.equalsIgnoreZeroSign(f4)
请注意,对于非原始类型,==映射到.equals()。
# 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 | T | - | Y |
double | N | C | C | C | C | T | Y | - |
- “Y”表示Java可以进行的转换
- “C”表示 Java 在显式强制转换时可以进行的转换
- “T”表示 Java 可以进行的转换,但数据被截断
- “N”表示 Java 无法进行的转换
Groovy的类型转换规则:
'Y'
表示 Groovy 可以进行转换。'D'
表示在动态编译或显式转换时,Groovy 可以进行转换。'T'
表示 Groovy 可以进行转换,但数据会被截断。'B'
表示装箱/拆箱操作。'N'
表示 Groovy 无法进行转换。
在转换为布尔值/Boolean 时,截断使用 Groovy Truth (opens new window) 进行转换。将数字转换为字符时,会将 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(在闭包中使用)
与Java相比,Groovy的限制较少,允许某些关键字出现在Java中非法的位置。例如,以下语句是有效的:var var = [def: 1, as: 2, in: 3, trait: 4]
。然而,尽管编译器可能能够正常工作,但不建议在可能引起混淆的位置使用上述关键字。特别是避免将它们用作变量、方法和类名,因此,之前的var var
示例将被视为不良风格。
关键字的详细文档可参考 (opens new window)。
# 现有Java项目集成Groovy
主要使用Groovy的Maven插件,将Groovy代码编译成Java字节码,然后打包到Java项目中。
这里用到Maven Group: Apache Groovy (opens new window)和gmavenplus-plugin (opens new window)
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>3.0.17</version>
</dependency>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>2.1.0</version>
<executions>
<execution>
<goals>
<goal>addSources</goal>
<goal>compile</goal>
</goals>
</execution>
</executions>
<configuration>
<sources>
<source>
<directory>src/main/groovy</directory>
<includes>
<include>**/*.groovy</include>
</includes>
</source>
</sources>
</configuration>
</plugin>
# Groovy的新编程范式
一个语言的强大主要体现在它的编程范式上,Groovy引入了许多新的编程范式,使得编写代码变得更加简单和高效。
# 1. 动态类型系统
Groovy的一个重要特性是其动态类型系统。在很多编程语言中,如Java,必须在变量声明时指定其类型,这个类型在后续的代码中不能改变。然而,Groovy采用的是动态类型系统,你可以在声明变量时不指定类型,Groovy会在运行时确定变量的类型。
动态类型的好处之一是编程灵活性增强。你不必提前知道或决定一个变量将包含什么类型的数据。这使得代码更加简洁,易于阅读和编写。
例如,下面是一个动态类型的Groovy例子:
def x = 123
println(x.getClass()) // 输出:class java.lang.Integer
x = "Hello, Groovy!"
println(x.getClass()) // 输出:class java.lang.String
在这个例子中,变量x
最初被赋值为一个整数,后来被重新赋值为一个字符串。在每一步,Groovy都正确地处理了变量的类型。
动态类型的另一个优点是它使得Groovy更适合脚本编程和快速原型设计。你可以快速地写出一段代码,测试一个想法,而无需花费大量时间来定义和管理变量的类型。
# 2. GString(Groovy字符串)
在Groovy中,字符串是一个非常强大的数据类型,其中的GString(Groovy字符串)提供了许多强大的功能。
GString是Groovy中的字符串类型,它与Java的String有一些相似之处,但也有一些独特的特性。GString的一个主要特性是它支持字符串插值(String Interpolation)。
字符串插值是指在一个字符串中插入表达式,当字符串被求值时,这些表达式也会被求值,并将结果插入到字符串中。这使得构建复杂的字符串变得非常简单。例如:
def name = "Groovy"
def message = "Hello, ${name}!"
println(message) // 输出:Hello, Groovy!
在这个例子中,${name}
是一个表达式,它在字符串被打印时被求值,并将结果插入到字符串中。
GString还支持多行字符串,这使得编写包含多行文本的字符串变得更简单。例如:
def multiLineString = """
This is a
multi-line
string.
"""
println(multiLineString)
在这个例子中,"""
被用来定义一个多行字符串。
另外,GString也支持使用+
和*
操作符进行字符串的拼接和重复。例如:
def hello = "Hello, " + "Groovy!" // 字符串拼接
println(hello)
def repeat = "Groovy " * 3 // 字符串重复
println(repeat) // 输出:Groovy Groovy Groovy
# 3. 操作符重载
Groovy允许您重载各种操作符,以便可以在您自己的类中使用它们。考虑下面这个简单的类:
class Bucket {
int size
Bucket(int size) {
this.size = size
}
Bucket plus(Bucket other) {
return new Bucket(this.size + other.size)
}
}
Bucket
实现了一个名为plus()
的特殊方法。通过实现plus()
方法,Bucket
类现在可以像这样使用+
操作符:
def b1 = new Bucket(4)
def b2 = new Bucket(11)
assert (b1 + b2).size == 15
可以使用+
操作符将两个Bucket
对象相加。
所有(非比较器)Groovy操作符都有对应的方法,您可以在自己的类中实现这些方法。唯一的要求是您的方法必须是公共的,具有正确的名称和正确数量的参数。参数类型取决于您想要在操作符的右侧支持的类型。例如,通过使用以下签名的plus()
方法:
Bucket plus(int capacity) {
return new Bucket(this.size + capacity)
}
您可以支持以下语句:
assert (b1 + 11).size == 15
这是操作符及其对应方法的完整列表:
操作符 | 方法 | 操作符 | 方法 |
---|---|---|---|
+ | a.plus(b) | [] | a.getAt(b) |
- | a.minus(b) | [] = c | a.putAt(b, c) |
* | a.multiply(b) | in | a in b |
/ | a.div(b) | << | a.leftShift(b) |
% | a.mod(b) | >> | a.rightShift(b) |
** | a.power(b) | >>> | a.rightShiftUnsigned(b) |
\ | a.or(b) | ++ | |
& | a.and(b) | -- | a.previous() |
^ | a.xor(b) | +a | a.positive() |
as | a.asType(b) | -a | a.negative() |
a() | a.call() | ~a | a.bitwiseNegate() |
通过重载这些操作符中的方法,您可以自定义类的操作符行为,以便更自然地处理对象。
# 4. 类方法扩展
在Groovy中,您可以使用类方法扩展(Class Methods Extension)为现有的类添加新的方法,而无需修改原始类的定义。这使您能够在不改变类的源代码的情况下,向类添加自定义行为。
类方法扩展的一般步骤如下:
创建一个静态方法,并将要扩展的类作为第一个参数。方法可以定义在任何Groovy类中,例如Groovy脚本、Groovy类文件或Groovy的扩展模块。
在方法体内部,可以使用
this
关键字引用要扩展的对象实例。注意,扩展方法只能访问对象的公共成员。调用扩展方法时,Groovy会自动将方法绑定到对应的对象上,使得该对象可以调用扩展方法。
以下是一个示例,展示了如何使用类方法扩展为String
类添加一个startsWithIgnoreCase()
方法:
class StringExtensions {
static boolean startsWithIgnoreCase(String str, String prefix) {
str.toLowerCase().startsWith(prefix.toLowerCase())
}
}
use(StringExtensions) {
def text = "Hello, world!"
println text.startsWithIgnoreCase("hello")
}
输出结果:
true
在上述示例中,创建了一个名为StringExtensions
的静态类,其中包含了一个名为startsWithIgnoreCase()
的静态方法。该方法接收一个String
类型的参数作为要检查的字符串,以及一个String
类型的参数作为要比较的前缀。在方法体内部,将两个字符串都转换为小写,并使用startsWith()
方法进行比较。然后,通过use
方法将StringExtensions
类应用为类方法扩展,使得可以直接在字符串对象上调用startsWithIgnoreCase()
方法。
使用类方法扩展时,需要注意以下几点:
- 类方法扩展只在应用的范围内生效,例如使用
use
方法指定的范围。 - 避免在不同的模块中定义相同名称的类方法扩展,以避免冲突。
- 类方法扩展应该遵循适当的命名约定,以确保代码的可读性和维护性。
类方法扩展是Groovy强大而灵活的特性之一,使您能够轻松地为现有的类添加新的方法,以满足特定的需求,提高代码的可重用性和扩展性。
# 5. 元数据编程
# a 运行时元数据编程
Groovy的运行时元数据编程主要依赖于MetaClass系统和GroovyObject接口。所有的Groovy对象都默认实现了GroovyObject接口,这个接口提供了动态添加方法和属性的能力。
# 1. GroovyObject介绍
GroovyObject是所有Groovy类的基类,它定义了几个用于动态方法和属性处理的方法,如 getProperty
,setProperty
,invokeMethod
等。在实际编程中,你几乎不会直接使用GroovyObject,但是需要理解其作用。
class MyGroovyClass {
def invokeMethod(String name, Object args) {
println("Called ${name} with ${args}")
}
}
def obj = new MyGroovyClass()
obj.someMethod("test") // 输出:Called someMethod with test
在这个例子中,MyGroovyClass重写了GroovyObject的invokeMethod方法,使得任何对未定义方法的调用都会被捕获,并打印出方法名和参数。
# 2. 方法缺失处理
当你尝试调用一个未定义的方法时,Groovy会调用methodMissing
方法。你可以重写这个方法来处理未定义的方法调用。
class MethodMissingExample {
def methodMissing(String name, args) {
println("Attempted to call ${name} with ${args}")
}
}
def example = new MethodMissingExample()
example.unknownMethod("test") // 输出:Attempted to call unknownMethod with [test]
在这个例子中,调用了一个未定义的方法unknownMethod
,Groovy捕获了这个调用并调用了重写的methodMissing
方法。
# 3. 字段缺失处理
类似地,当你尝试访问一个未定义的字段时,Groovy会调用propertyMissing
方法。你可以重写这个方法来处理未定义的字段访问。
class PropertyMissingExample {
def propertyMissing(String name) {
println("Attempted to access ${name}")
}
}
def example = new PropertyMissingExample()
println(example.unknownProperty) // 输出:Attempted to access unknownProperty
在这个例子中,尝试访问一个未定义的属性unknownProperty
,Groovy捕获了这个访问并调用了重写的propertyMissing
方法。
# 4. 静态方法缺失处理
你也可以处理对未定义的静态方法的调用,通过重写staticMethodMissing
方法。
class StaticMethodMissingExample {
static def staticMethodMissing(String name, args) {
println("Attempted to call static ${name} with ${args}")
}
}
StaticMethodMissingExample.unknownStaticMethod("test") // 输出:Attempted to call static unknownStaticMethod with [test]
在这个例子中,尝试调用一个未定义的静态方法unknownStaticMethod
,Groovy捕获了这个调用并调用了重写的staticMethodMissing
方法。
# 5. 方法拦截
你可以通过重写invokeMethod
方法来拦截所有的方法调用。
class InvokeMethodExample {
def invokeMethod(String name, args) {
println("Attempted to call ${name} with ${args}")
}
}
def example = new InvokeMethodExample()
example.anyMethod("test") // 输出:Attempted to call anyMethod with [test]
在这个例子中,调用了一个任意的方法anyMethod
,Groovy捕获了这个调用并调用了重写的invokeMethod
方法。
# 6. 方法扩展
你可以向任何对象添加新的方法。这是通过修改对象的metaClass
来实现的。
String.metaClass.shout = { -> delegate.toUpperCase() + "!" }
println("hello".shout()) // 输出:HELLO!
在这个例子中,向String
类添加了一个shout
方法,这个方法将字符串转换为大写并添加一个感叹号。
# 7. 动态访问属性和方法
通过getProperty
和setProperty
方法,Groovy支持动态地访问和修改属性。类似地,invokeMethod
方法允许动态地调用方法。
class DynamicAccessExample {
def getProperty(String name) {
println("Getting ${name}")
}
def setProperty(String name, value) {
println("Setting ${name} to ${value}")
}
def invokeMethod(String name, args) {
println("Calling ${name} with ${args}")
}
}
def example = new DynamicAccessExample()
example.someProperty = "test" // 输出:Setting someProperty to test
println(example.someProperty) // 输出:Getting someProperty
example.someMethod("test") // 输出:Calling someMethod with [test]
在这个例子中,动态地访问和修改了一个属性,并动态地调用了一个方法。
# 8. GroovyInterceptable接口
GroovyInterceptable
接口提供了一种更灵活的方法拦截机制。当一个类实现了这个接口,所有对这个类的方法的调用都会被拦截,即使这个方法已经在类中定义。
class InterceptableExample implements GroovyInterceptable {
def invokeMethod(String name, args) {
println("Intercepted call to ${name} with ${args}")
}
}
def example = new InterceptableExample()
example.someMethod("test") // 输出:Intercepted call to someMethod with [test]
在这个例子中,调用了一个任意的方法someMethod
,尽管这个方法并未在类中定义,但是由于类实现了GroovyInterceptable
接口,这个调用仍然被拦截。
# 9. 类别
Groovy的类别(Categories)允许你向一个类添加新的方法,但是这些方法只在类别的作用范围内有效。这是一种局部的方法扩展。
class StringCategory {
static String shout(String self) {
return self.toUpperCase() + "!"
}
}
use(StringCategory) {
println("hello".shout()) // 输出:HELLO!
}
println("hello".shout()) // 抛出异常:No signature of method...
在这个例子中,向String
类添加了一个shout
方法,但是这个方法只在use(StringCategory)
的作用范围内有效。
也可以使用@Category
注解来定义类别。
@Category(String)
class StringUtilCategory {
static String shout(String self) {
return self.toUpperCase() + "!"
}
}
# 10. MetaClass介绍
每个Groovy对象都有一个元类(MetaClass),元类包含了这个对象的所有方法和属性。你可以通过修改元类来添加、修改或删除方法和属性。
String.metaClass.shout = { -> delegate.toUpperCase() + "!" }
println("hello".shout()) // 输出:HELLO!
在这个例子中,向String
类的元类添加了一个shout
方法,这个方法将字符串转换为大写并添加一个感叹号。
ExpandoMetaClass是Groovy的一个内置类,它允许你更灵活地修改元类。例如,你可以向一个类的所有实例添加新的方法,而不仅仅是一个对象。
String.metaClass = new ExpandoMetaClass(String, false, true)
String.metaClass.shout = { -> delegate.toUpperCase() + "!" }
println("hello".shout()) // 输出:HELLO!
println("world".shout()) // 输出:WORLD!
在这个例子中,向String
类的所有实例添加了一个shout
方法,而不仅仅是"hello"字符串对象。
# 11. 属性扩展
你可以通过修改元类来向任何对象添加新的属性。
String.metaClass.category = "Text"
println("hello".category) // 输出:Text
在这个例子中,向String
类添加了一个category
属性,这个属性对所有的String
对象都有效。
# 12. 构造函数扩展
你可以通过修改元类来向任何对象添加新的构造函数。
String.metaClass.constructor = { int count, char c -> String.valueOf(Character.toChars(c)).multiply(count) }
println(new String(5, 'a')) // 输出:aaaaa
在这个例子中,向String
类添加了一个新的构造函数,这个构造函数接收一个数字和一个字符,然后生成一个包含指定数量字符的字符串。
# 13. 方法扩展
你可以通过修改元类来向任何类添加新的静态方法。
String.metaClass.static.fromNumber = { int number -> number.toString() }
println(String.fromNumber(123)) // 输出:123
在这个例子中,向String
类添加了一个fromNumber
静态方法,这个方法将一个数字转换为字符串。
如果是实例方法扩展,则需要去掉static
关键字。
class Book {
String title
}
Book.metaClass.titleInUpperCase << {-> title.toUpperCase() }
def b = new Book(title:"The Stand")
assert "THE STAND" == b.titleInUpperCase()
# 14. 方法借用
你可以方法指针语法“借用”其他类的方法,使得这些方法在当前类中可用。
class Person {
String name
}
class MortgageLender {
def borrowMoney() {
"buy house"
}
}
def lender = new MortgageLender()
Person.metaClass.buyHouse = lender.&borrowMoney
def p = new Person()
assert "buy house" == p.buyHouse()
在这个例子中,“借用”了BorrowMethodsExample
类的所有方法,并在MyGroovyClass
中使用这些方法。
# 15. 动态方法名称
你可以动态地生成方法名称,然后调用这些方法。
class Person {
String name = "Fred"
}
def methodName = "Bob"
Person.metaClass."changeNameTo${methodName}" = {-> delegate.name = "Bob" }
def p = new Person()
assert "Fred" == p.name
p.changeNameToBob()
assert "Bob" == p.name
在这个例子中,动态地生成了一个方法名称,然后调用了这个方法。
# 16. 运行时方法和属性发现
你可以在运行时发现一个对象的所有方法和属性。
class MethodAndPropertyDiscoveryExample {
def exampleMethod() { }
def exampleProperty
}
def example = new MethodAndPropertyDiscoveryExample()
println(example.metaClass.methods*.name) // 输出所有方法的名称
println(example.metaClass.properties*.name) // 输出所有属性的名称
在这个例子中,打印出了一个对象的所有方法和属性的名称。
# 17. 方法拦截
你可以拦截对静态方法的调用,通过重写invokeStaticMethod
方法。
class StaticMethodInterceptionExample {
static def exampleMethod() { }
}
StaticMethodInterceptionExample.metaClass.invokeStaticMethod = { Object object, String method, Object[] arguments ->
println("Attempted to call static ${method} on ${object.name} with ${arguments}")
}
StaticMethodInterceptionExample.exampleMethod() // 输出:Attempted to call static exampleMethod on StaticMethodInterceptionExample with []
在这个例子中,尝试调用一个静态方法,Groovy捕获了这个调用并调用了重写的invokeStaticMethod
方法。
拦截示例方法调用,通过重写invokeMethod
方法。
class Stuff {
def invokeMe() { "foo" }
}
Stuff.metaClass.invokeMethod = { String name, args ->
def metaMethod = Stuff.metaClass.getMetaMethod(name, args)
def result
if(metaMethod) result = metaMethod.invoke(delegate,args)
else {
result = "bar"
}
result
}
def stf = new Stuff()
assert "foo" == stf.invokeMe()
assert "bar" == stf.doStuff()
# 18. 属性拦截
class Person {
String name = "Fred"
}
Person.metaClass.getProperty = { String name ->
def metaProperty = Person.metaClass.getMetaProperty(name)
def result
if(metaProperty) result = metaProperty.getProperty(delegate)
else {
result = "Flintstone"
}
result
}
def p = new Person()
assert "Fred" == p.name
assert "Flintstone" == p.other
# b 编译时元数据编程
# 1. 可用的AST转换
Groovy提供了一些内置的AST转换,你可以通过注解来使用这些转换。
例如,@ToString
注解会自动生成toString
方法。
import groovy.transform.ToString
@ToString
class ToStringExample {
def name
def value
}
def example = new ToStringExample(name: "test", value: 123)
println(example) // 输出:ToStringExample(test, 123)
在这个例子中,使用@ToString
注解生成了一个toString
方法,这个方法返回对象的名称和值。
同样,Groovy还提供了一些其他的AST转换,例如@TupleConstructor
(自动生成一个元组构造函数)、@Canonical
(自动生成equals
、hashCode
和toString
方法)、@Lazy
(延迟初始化属性)、@Sortable
(自动生成比较方法)和@Delegate
(委托方法的实现)。
# 2. 开发AST转换
这里只介绍本地AST转换,关于全局AST转换请参考:Global transformations (opens new window)。
创建注解和对应的ASTTransformation:
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.METHOD])
@GroovyASTTransformationClass(classes = WithLoggingASTTransformation)
@interface WithLogging {
}
import groovy.transform.CompileStatic
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.expr.ArgumentListExpression
import org.codehaus.groovy.ast.expr.ConstantExpression
import org.codehaus.groovy.ast.expr.MethodCallExpression
import org.codehaus.groovy.ast.expr.VariableExpression
import org.codehaus.groovy.ast.stmt.BlockStatement
import org.codehaus.groovy.ast.stmt.ExpressionStatement
import org.codehaus.groovy.ast.stmt.Statement
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformation
@CompileStatic
@GroovyASTTransformation(phase= CompilePhase.SEMANTIC_ANALYSIS)
class WithLoggingASTTransformation implements ASTTransformation {
@Override
void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
MethodNode method = (MethodNode) nodes[1]
def startMessage = createPrintlnAst("Starting $method.name")
def endMessage = createPrintlnAst("Ending $method.name")
def existingStatements = ((BlockStatement)method.code).statements
existingStatements.add(0, startMessage)
existingStatements.add(endMessage)
}
private static Statement createPrintlnAst(String message) {
new ExpressionStatement(
new MethodCallExpression(
new VariableExpression("this"),
new ConstantExpression("println"),
new ArgumentListExpression(
new ConstantExpression(message)
)
)
)
}
}
测试:
@WithLogging
def greet() {
println "Hello World"
}
greet()
输出:
Starting greet
Hello World
Ending greet
# 6. 生成器
Groovy 中的生成器是用于简化复杂对象创建的强大工具。这些对象可以是任何复杂的数据结构,例如 XML 或 JSON 数据,或者 UI 组件等。
# 1. XML 生成器
XML 生成器可以让你以更自然、更易读的方式生成 XML。以下是一个简单的例子:
import groovy.xml.MarkupBuilder
def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
xml.person(id: '1') {
name("John Doe")
email("john.doe@example.com")
}
println writer.toString()
输出结果:
<person id='1'>
<name>John Doe</name>
<email>john.doe@example.com</email>
</person>
# 2. JSON 生成器
同样,Groovy 也提供了 JSON 生成器:
import groovy.json.JsonBuilder
def json = new JsonBuilder()
json.person {
id 1
name "John Doe"
email "john.doe@example.com"
}
println json.toPrettyString()
输出结果:
{
"person": {
"id": 1,
"name": "John Doe",
"email": "john.doe@example.com"
}
}
# 3. 使用元编程定制生成器
除了使用Groovy内置的生成器,你也可以使用元编程技术来创建自定义的生成器。这通常通过实现methodMissing
或者修改MetaClass
来完成。
下面是一个简单的例子,展示了如何创建一个HTML生成器:
class HtmlBuilder {
def out = new StringWriter()
def methodMissing(String name, args) {
out << "<$name>"
if (args.length > 0 && args[0] instanceof Closure) {
args[0].delegate = this
args[0].call()
}
out << "</$name>\n"
}
String toString() {
out.toString()
}
}
def html = new HtmlBuilder()
html.html {
head {
title { 'Hello, World!' }
}
body {
h1 { 'Welcome to my website' }
p { 'This is a paragraph.' }
}
}
println html
在这个例子中,创建了一个HtmlBuilder
类,这个类重写了methodMissing
方法,使得可以使用任何方法来表示HTML标签。使用闭包来表示标签的内容,闭包的委托对象就是HTML生成器,所以可以在闭包中使用任何方法。
输出的HTML将会是:
<html>
<head>
<title>Hello, World!</title>
</head>
<body>
<h1>Welcome to my website</h1>
<p>This is a paragraph.</p>
</body>
</html>
这只是一个基础的例子,实际的HTML生成器会更复杂。例如,你可能需要处理属性、自闭合标签,或者使用缩进来改善输出的可读性。
通过元编程,你可以创建任何类型的生成器。你只需要了解你想要生成的结构的语法,然后使用Groovy的动态特性来模拟这个语法。
# 7. 脚本化编程
# 1. 脚本文件和命令行执行
Groovy 的一个强大特性是它支持脚本化编程。你可以将 Groovy 代码放入 .groovy
文件中,然后像运行其他脚本语言(如 Python 或 Shell 脚本)一样来运行它。例如,假设你有一个文件名为 script.groovy
的 Groovy 脚本,你可以使用以下命令来运行它:
groovy script.groovy
此外,你可以在命令行中直接执行 Groovy 代码。例如:
groovy -e "println 'Hello, World!'"
这将在控制台打印出 "Hello, World!"。
# 2. DSL(领域特定语言)支持
Groovy 提供了创建 DSL(领域特定语言)的强大支持。DSL 是为特定问题领域设计的计算机语言,它使用特定领域的术语和概念,从而使领域专家能够直接与程序交互,而不需要了解编程知识。
通过 Groovy 的动态特性和闭包,你可以创建流畅易读的 DSL。以下是一个简单的例子,定义了一个 DSL 用于描述饮料:
class Beverage {
String name
float price
static Beverage drink(Closure closure) {
Beverage beverage = new Beverage()
closure.delegate = beverage
closure.call()
return beverage
}
def named(String name) { this.name = name }
def costs(float price) { this.price = price }
}
Beverage beverage = Beverage.drink {
named 'Coffee'
costs 5.00
}
println "${beverage.name} costs \$${beverage.price}"
在这个例子中,定义了一个 Beverage
类,它有两个方法:named
和 costs
。使用这两个方法在 drink
方法的闭包中描述一个饮料。输出将是 "Coffee costs $5.0"。
典型的DSL应用就是Gradle构建脚本。Gradle使用Groovy作为构建脚本的语言,这使得Gradle构建脚本非常易读。
# 3. Groovy脚本在Java应用中的应用场景
这里主要介绍GroovyScriptEngine
的使用。它提供了一种在运行时动态加载和执行Groovy脚本的机制。
以下是一个简单的示例,展示了GroovyScriptEngine的使用:
import groovy.util.GroovyScriptEngine;
import groovy.lang.Binding;
public class GroovyScriptExample {
public static void main(String[] args) throws Exception {
// 创建GroovyScriptEngine对象,指定脚本存储位置
String[] roots = { "scripts/" }; // 脚本存储在scripts目录下
GroovyScriptEngine scriptEngine = new GroovyScriptEngine(roots);
// 创建Binding对象,用于传递变量给脚本
Binding binding = new Binding();
binding.setVariable("name", "Alice");
// 执行脚本
scriptEngine.run("greeting.groovy", binding);
// 从脚本中获取结果
String result = (String) binding.getVariable("result");
System.out.println(result);
}
}
假设在scripts/
目录下有一个名为greeting.groovy
的脚本文件,其内容如下:
def greeting = "Hello, $name!"
result = greeting.toUpperCase()
运行上述Java代码,将会输出:
HELLO, ALICE!
# 测试框架 Spock
Spock 是一个基于 Groovy 的测试框架,它提供了一种更清晰、更直观的方式来编写测试,尤其是单元测试。
与 JUnit 相比,Spock 提供了更高级的功能,例如参数化测试、异常测试和强大的 mock 框架。
下面是一个 Spock 测试的基本结构:
import spock.lang.Specification
class SimpleTest extends Specification {
def "length of Spock's and his friends' names"() {
expect:
"Spock".length() == 5
"Kirk".length() == 4
"Scotty".length() == 6
}
}
在这个例子中,创建了一个 Spock 测试,测试了几个字符串的长度。注意,使用了描述性的字符串作为测试方法的名称,这使得测试的目的更明确。
# 1. 参数化测试
Spock 支持参数化测试,你可以使用数据表格来为测试方法提供不同的参数:
class ParameterizedTest extends Specification {
def "maximum of #a and #b is #c"() {
expect:
Math.max(a, b) == c
where:
a | b || c
3 | 7 || 7
5 | 4 || 5
9 | 9 || 9
}
}
在这个例子中,测试了 Math.max
方法。每一行数据都会创建一个新的测试,a
、b
和 c
的值都会从数据表格中取得。
# 2. 异常测试
Spock 可以方便地测试异常。例如:
class ExceptionTest extends Specification {
def "should throw NullPointerException when doSomething is called"() {
when:
doSomething(null)
then:
thrown(NullPointerException)
}
}
在这个例子中,测试了 doSomething
方法当传入 null
参数时是否抛出 NullPointerException
。
# 3. Mocking 和 Stubbing
Spock 提供了强大的 mocking 和 stubbing 功能。你可以创建 mock 对象,然后定义这些对象的行为,或者验证它们是否被正确地调用。
class MockingTest extends Specification {
def "should call the save method of the repository"() {
given:
def repository = Mock(Repository)
def service = new Service(repository)
when:
service.save("data")
then:
1 * repository.save("data")
}
}
在这个例子中,创建了一个 Repository
的 mock 对象,然后验证了 Service.save
方法是否调用了 repository.save
。
Spock利用Groovy的AST转换机制将易读的测试规范转换为标准的JUnit测试方法,然后使用自定义的运行时拦截器来解释和执行这些测试方法。
这种组合使得Spock能够提供更具可读性和表达力的测试语法,并与其他测试工具和框架无缝集成。
# 总结
Groovy是一种功能强大的动态编程语言,具有丰富的元编程特性和灵活的语法。
本文着重对其新编程范式,也就是Java中不支持的功能进行了介绍。
通过掌握这些Groovy的特性,您可以更好地利用Groovy的灵活性和强大的元编程能力,提高代码的可读性、表达力和可维护性。
同时,您也可以通过DSL和Spock等工具,编写清晰、可维护的单元测试和领域特定语言,提升开发效率和代码质量。
祝你变得更强!