本章将会介绍 Groovy 的程序结构

# 1. 包名

包名的作用与 Java 中的作用完全相同。它们使我们能够在没有任何冲突的情况下分离代码库。Groovy 类必须在类定义之前指定它们的包,否则就采用默认的包。

定义包名和 Java 中非常相似:

// 定义一个名为 com.yoursite 的包
package com.yoursite

要引用com.yoursite.com包中的某个类Foo,你需要使用完全限定的名称com.yoursite.com.Foo,或者你可以使用导入语句,我们将在下面看到。

# 2. 导入

为了引用任何一个类,你需要对它的包进行限定引用。Groovy 遵循 Java 的概念,允许使用导入语句来解析类引用。

例如,Groovy 提供了几个构建类,比如MarkupBuilderMarkupBuilder在包groovy.xml里面,所以为了使用这个类,你需要导入它,如下所示:

// 导入 MarkupBuilder 类
import groovy.xml.MarkupBuilder

// 使用导入的类创建一个对象
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 就是一个简单的导入,它直接引用一个包内的类。

// 导入 MarkupBuilder 类
import groovy.xml.MarkupBuilder

// 使用导入的类创建一个对象
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

这是完全有效的代码。但是通过*的导入,我们只需要一行就可以达到同样的效果。star导入了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类中有名为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 与 script

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:main 的方法体

这是典型的来自 Java 的代码,代码必须嵌入到一个类中才能执行。Groovy 让它变得更简单,下面的代码含义与上面相同:

Main.groovy

println 'Groovy world!'

脚本可以看作是一个类,而不需要声明它,这是脚本与类的一部分区别。

# 3.2 脚本类

脚本总是被编译成一个类。Groovy 编译器将为你编译这个类,并将脚本的主体复制到一个运行方法中。因此,前面的例子被编译成了下面的样子:

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

你也可以将方法和代码混合在一起。生成的脚本类会将所有方法带入脚本类,并将所有脚本体组装到运行方法中:

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:第一条语句被复制到运行方法中
  • 注释 3:第二条语句被复制到运行方法中

即使 Groovy 从你的脚本中创建一个类,对用户来说也是完全透明的。特别是,脚本会被编译成字节码,并且保留了行号。这意味着,如果在脚本中抛出异常,堆栈跟踪将显示对应于原始脚本的行号,而不是我们显示的生成代码。

# 3.4 变量

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

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

将表现为:

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

然而两者在语义上是有区别的:

  • 如果变量的声明和第一个例子一样,那么它就是一个局部变量。它将在编译器生成的run方法中声明,在脚本主体之外将不可见。特别要注意:这样的变量在脚本的其他方法中是不可见的。
  • 如果变量未声明,它就会进入脚本绑定中。绑定在方法中是可见的,如果你使用脚本与应用程序交互,并且需要在脚本和应用程序之间共享数据,那么绑定就显得尤为重要。读者可以参考集成指南了解更多信息。

如果你想让一个变量在不进入Binding的情况下成为类的一个字段,你可以使用@Field注解。