程式結構

本章節介紹 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 中的相同程式碼需要一個匯入陳述式來匯入 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 有名為 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 與腳本

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 變數同名的區域變數或腳本欄位,你可以使用 binding.varName 存取 binding 變數。