程序结构

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

1. 包名

包名与 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 提供了几个构建器类,例如 `MarkupBuilder`。`MarkupBuilder` 位于 `groovy.xml` 包中,因此为了使用此 P类,你需要像这样导入它

// 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 中的相同代码需要一个导入 `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 一样,提供了一种特殊的方式来使用 `*` 导入包中的所有类,这称为星号导入。`MarkupBuilder` 是 `groovy.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` 关键字的静态导入为命名空间问题提供了优雅的解决方案。假设你想使用 `Calendar` 实例的 `getInstance()` 方法。它是一个静态方法,所以我们可以使用静态导入。但是,与其每次都调用 `getInstance()`(当与类名分离时可能会产生误导),我们可以使用别名导入它,以提高代码可读性

import static Calendar.getInstance as now

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

现在,这很干净!

2.6. 静态星号导入

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

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

import static java.lang.Math.*

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

如你所见,我们可以直接访问 `sin` 和 `cos` 方法,而无需 `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 与 脚本

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` 来访问绑定变量。