程序结构

本章介绍 Groovy 编程语言的程序结构。

1. 包名

包名在 Groovy 中的作用与 Java 相同。它们允许我们分离代码库,避免冲突。Groovy 类必须在类定义之前指定其包,否则将假定为默认包。

定义包与 Java 非常相似

// defining a package named com.yoursite
package com.yoursite

要引用 com.yoursite.com 包中的某个类 Foo,您需要使用完全限定名 com.yoursite.com.Foo,或者使用 import 语句,如下所示。

2. 导入

为了引用任何类,您需要一个指向其包的限定引用。Groovy 遵循 Java 的概念,允许 import 语句解析类引用。

例如,Groovy 提供了几个构建器类,例如 MarkupBuilderMarkupBuilder 位于 groovy.xml 包中,因此为了使用此类,您需要像下面这样 import

// importing the class MarkupBuilder
import groovy.xml.MarkupBuilder

// using the imported class to create an object
def xml = new MarkupBuilder()

assert xml != null

2.1. 默认导入

默认导入是 Groovy 语言默认提供的导入。例如,请查看以下代码

new Date()

在 Java 中,相同的代码需要一个 import 语句来导入 Date 类,如下所示:import java.util.Date。Groovy 默认为您导入这些类。

下面这些导入是 groovy 为您添加的

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

这样做是因为这些包中的类是最常用的。通过导入这些类,可以减少样板代码。

2.2. 简单导入

简单导入是您完全定义类名及其包的导入语句。例如,下面代码中的 import groovy.xml.MarkupBuilder 导入语句就是一个简单导入,它直接引用了包中的一个类。

// importing the class MarkupBuilder
import groovy.xml.MarkupBuilder

// using the imported class to create an object
def xml = new MarkupBuilder()

assert xml != null

2.3. 星号导入

Groovy 与 Java 一样,提供了一种特殊的方法来使用 * 导入包中的所有类,即所谓的星号导入。MarkupBuildergroovy.xml 包中的一个类,它与另一个名为 StreamingMarkupBuilder 的类一起存在。如果您需要使用这两个类,您可以这样做

import groovy.xml.MarkupBuilder
import groovy.xml.StreamingMarkupBuilder

def markupBuilder = new MarkupBuilder()

assert markupBuilder != null

assert new StreamingMarkupBuilder() != null 

这完全是有效的代码。但是使用 * 导入,我们只需一行代码就可以实现相同的效果。星号会导入 groovy.xml 包下的所有类

import groovy.xml.*

def markupBuilder = new MarkupBuilder()

assert markupBuilder != null

assert new StreamingMarkupBuilder() != null

* 导入的一个问题是它们会使您的本地命名空间混乱。但是,借助 Groovy 提供的别名,这个问题可以轻松解决。

2.4. 静态导入

Groovy 的静态导入功能允许您引用导入的类,就好像它们是您自己类中的静态方法一样

import static Boolean.FALSE

assert !FALSE //use directly, without Boolean prefix!

这与 Java 的静态导入功能类似,但它比 Java 更动态,因为它允许您定义与导入方法同名的方法,只要它们具有不同的类型即可

import static java.lang.String.format (1)

class SomeClass {

    String format(Integer i) { (2)
        i.toString()
    }

    static void main(String[] args) {
        assert format('String') == 'String' (3)
        assert new SomeClass().format(Integer.valueOf(1)) == '1'
    }
}
1 方法的静态导入
2 在上面静态导入方法的同一个名称下声明方法,但参数类型不同
3 Java 中的编译错误,但这是有效的 Groovy 代码

如果类型相同,则导入的类优先。

2.5. 静态导入别名

使用 as 关键字进行静态导入为命名空间问题提供了一个优雅的解决方案。假设您想使用 CalendargetInstance() 方法获取 Calendar 实例。这是一个静态方法,因此我们可以使用静态导入。但是,我们不想每次都调用 getInstance(),因为它与类名分开可能会造成误解,所以我们可以使用别名导入它,以提高代码的可读性

import static Calendar.getInstance as now

assert now().class == Calendar.getInstance().class

现在,代码简洁明了!

2.6. 静态星号导入

静态星号导入与普通星号导入非常相似。它将导入给定类中的所有静态方法。

例如,假设我们需要为我们的应用程序计算正弦和余弦。java.lang.Math 类具有名为 sincos 的静态方法,它们满足我们的需求。借助静态星号导入,我们可以这样做

import static java.lang.Math.*

assert sin(0) == 0.0
assert cos(0) == 1.0

如您所见,我们可以直接访问 sincos 方法,无需使用 Math. 前缀。

2.7. 导入别名

使用类型别名,我们可以使用我们选择的名称引用完全限定的类名。这可以通过 as 关键字实现,与之前一样。

例如,我们可以将 java.sql.Date 导入为 SQLDate,并将其与 java.util.Date 在同一个文件中使用,无需使用这两个类的完全限定名

import java.util.Date
import java.sql.Date as SQLDate

Date utilDate = new Date(1000L)
SQLDate sqlDate = new SQLDate(1000L)

assert utilDate instanceof java.util.Date
assert sqlDate instanceof java.sql.Date

3. 脚本与类

3.1. public static void main vs 脚本

Groovy 支持脚本和类。例如,请查看以下代码

Main.groovy
class Main {                                    (1)
    static void main(String... args) {          (2)
        println 'Groovy world!'                 (3)
    }
}
1 定义一个 Main 类,名称是任意的
2 public static void main(String[]) 方法可用作类的主方法
3 方法的主体

这是您通常从 Java 代码中看到的典型代码,其中代码必须嵌入到类中才能执行。Groovy 使得操作更加容易,以下代码等效

Main.groovy
println 'Groovy world!'

脚本可以被认为是无需声明的类,但也有一些区别。

3.2. 脚本类

一个 groovy.lang.Script 始终被编译成一个类。Groovy 编译器将为您编译类,并将脚本主体复制到 run 方法中。因此,前面的示例被编译成以下内容

Main.groovy
import org.codehaus.groovy.runtime.InvokerHelper
class Main extends Script {                     (1)
    def run() {                                 (2)
        println 'Groovy world!'                 (3)
    }
    static void main(String[] args) {           (4)
        InvokerHelper.runScript(Main, args)     (5)
    }
}
1 Main 类扩展了 groovy.lang.Script
2 groovy.lang.Script 需要一个返回值为 run 的方法
3 脚本主体进入 run 方法
4 main 方法是自动生成的
5 并将脚本的执行委托给 run 方法

如果脚本位于文件中,则使用文件的基名来确定生成的脚本类的名称。在此示例中,如果文件的名称为 Main.groovy,则脚本类将为 Main

3.3. 方法

可以在脚本中定义方法,如这里所示

int fib(int n) {
    n < 2 ? 1 : fib(n-1) + fib(n-2)
}
assert fib(10)==89

您也可以混合使用方法和代码。生成的脚本类将把所有方法带入脚本类,并将所有脚本主体组合到 run 方法中

println 'Hello'                                 (1)

int power(int n) { 2**n }                       (2)

println "2^6==${power(6)}"                      (3)
1 脚本开始
2 在脚本主体中定义了一个方法
3 脚本继续

此代码在内部转换为

import org.codehaus.groovy.runtime.InvokerHelper
class Main extends Script {
    int power(int n) { 2** n}                   (1)
    def run() {
        println 'Hello'                         (2)
        println "2^6==${power(6)}"              (3)
    }
    static void main(String[] args) {
        InvokerHelper.runScript(Main, args)
    }
}
1 power 方法按原样复制到生成的脚本类中
2 第一个语句被复制到 run 方法中
3 第二个语句被复制到 run 方法中
即使 Groovy 从您的脚本中创建了一个类,对于用户来说也是完全透明的。尤其是,脚本被编译成字节码,并且行号被保留。这意味着如果脚本中抛出异常,堆栈跟踪将显示与原始脚本对应的行号,而不是我们显示的生成的代码。

3.4. 变量

脚本中的变量不需要类型定义。这意味着此脚本

int x = 1
int y = 2
assert x+y == 3

将与以下内容的行为相同

x = 1
y = 2
assert x+y == 3

但是,两者之间存在语义差异

  • 如果变量像第一个示例那样声明,它是一个局部变量。它将在编译器生成的 run 方法中声明,并且不会在脚本主体之外可见。特别是,这样的变量不会在脚本的其他方法中可见

  • 如果变量未声明,它将进入 groovy.lang.Script#getBinding()。绑定在方法中可见,如果使用脚本与应用程序交互并且需要在脚本和应用程序之间共享数据,则它尤为重要。读者可以参考 集成指南以获取更多信息。

使变量对所有方法可见的另一种方法是使用 @Field 注解。用这种方式注释的变量将成为生成的脚本类中的一个字段,并且,与局部变量一样,访问不会涉及脚本 Binding。虽然不推荐,但如果您有一个与绑定变量同名的局部变量或脚本字段,可以使用 binding.varName 访问绑定变量。