讀古今文學網 > Java程序員修煉之道 > 8.2 Groovy 101:語法和語義 >

8.2 Groovy 101:語法和語義

上一節只寫了一行Groovy語句,沒有任何類或方法之類的結構(用Java時會需要)。實際上你寫的是一個Groovy腳本。

Groovy腳本

跟Java不同,Groovy的源碼可以當做腳本執行。比如說,如果你有一段代碼放在類定義之外,那段代碼還是可以執行。像其他動態腳本語言(比如Ruby或Python)一樣,Groovy腳本在JVM上執行之前要在內存中經過完整的分析、編譯和生成過程。任何能在Groovy控制台執行的代碼都可以保存到.groovy文件,經過編譯後,就可以作為腳本運行。

一些開發人員已經用Groovy腳本取代了shell腳本,因為它們功能更強,更易於編寫,並且只要裝了JVM,就可以在任何平台上運行。給你一個性能方面的小提示,請使用groovyserv類庫,它會啟動JVM和Groovy擴展,讓腳本運行得更快。

Groovy的一個關鍵特性是可以使用跟Java中一樣的結構,語法也類似。為了突出這種相似性,請在Groovy控制台中執行下面這段類似Java的代碼:

public class PrintStatement
{
  public static void main(String args)
  {
    System.out.println(\"It\'s Groovy baby, yeah!\");
  }
}
  

結果和前面那個只有一行的Groovy腳本一樣,都是輸出\"It\'s Groovy baby, yeah!\"。除了使用Groovy控制台,你還可以把源碼放到PrintStatement.groovy源文件中,用groovyc編譯它,然後用groovy執行。換句話說,你能像Java中那樣帶著類和方法編寫Groovy源碼。

提示 在Groovy中幾乎可以使用所有Java普通語法,所以while/for循環、if/else結構、switch語句等,都會按你期望的方式工作。所有新語法及主要差異都會在本節及相應章節中重點闡述。

隨著本章內容的深入,我們會向你介紹Groovy特有的語法慣用語,例子也會從類似Java的語法向更純粹的Groovy語法轉變。你已經習慣了結構沉重的Java代碼,再見到像腳本一樣簡潔的Groovy語法,很容易發現兩者的差異。

本節的剩餘部分會介紹Groovy的基本語法和語義,以及它們為什麼能幫助開發人員。具體來說,我們會探討:

  • 默認導入;
  • 數字處理;
  • 變量、動態與靜態類型,以及作用域;
  • 列表與映射的語法。

首先,理解Groovy提供了哪些開箱即用的東西很重要。我們先來看看Groovy腳本或程序的默認導入。

8.2.1 默認導入

Groovy會默認導入一些語言包和工具包,以提供基本的語言支持。Groovy還會導入一系列的Java包,以便為其初始功能提供更廣泛的基礎。下面這個導入列表總是隱含在Groovy代碼之中:

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

要使用更多的包和類,可以像Java一樣用import語句。比如要從Java中得到所有Math類,只要在Groovy源碼裡加上import java.math.*;就行了。

設置可選的JAR文件

為了添加功能(比如內存數據庫及其驅動),可以在Groovy安裝中添加可選JAR。Groovy為此提供了一個慣用語:通常是在腳本中使用@Grab註解。另外一種辦法(在你仍在學習Groovy時)是效仿Java,把JAR文件加到CLASSPATH中。

下面就來使用一下默認的語言支持,並看看Java和Groovy在數字處理上的差異。

8.2.2 數字處理

Groovy能動態計算數學表達式,並且它採用最小意外原則。這一原則在處理浮點數時(比如3.2)尤其明顯。Groovy在底層用Java中的BigDecimal表示浮點數,但它會確保BigDecimal的行為盡量符合開發人員的期望。

1. Java和BigDecimal

我們來看一個經常會讓開發人員頭疼的數字處理問題。在Java中,如果在BigDecimal 3上加0.2,你覺得答案應該是什麼?缺乏經驗的Java開發人員在沒看Javadoc的情況下很可能會執行下面這種代碼,它會返回一個極其恐怖的結果:3.200000000000000011102230246251565404236316680908203125。

BigDecimal x = new BigDecimal(3);
BigDecimal y = new BigDecimal(0.2);
System.out.println(x.add(y));
  

經驗豐富的Java開發人員知道最好用BigDecimal(String val),而不是用將數字作為參數的BigDecimal 構造方法。以字符串為參數的構造方法寫出來的代碼會產生預期答案3.2:

BigDecimal x = new BigDecimal(\"3\");
BigDecimal y = new BigDecimal(\"0.2\");
System.out.println(x.add(y));
  

這有點悖於常理,所以Groovy默認採用了以字符串為參數的構造方法,解決了這一問題。

2. Groovy和BigDecimal

在Groovy中處理浮點數(在底層用BigDecimal表示)時,會自動使用以字符串為參數的構造方法,3 + 0.2會得到3.2。你可以在Groovy控制台中輸入下面的指令親自證實一下:

3 + 0.2;
  

你會發現Groovy對BEDMAS1的支持是正確的。並且在需要時能無縫切換數字類型(比如intdouble)。

1 回想起你在學校的日子了吧!BEDMAS表示括號、次方、除法、乘法、加法和減法,是我們計算數學題目時所要遵循的順序(先計算括號和次方,再計算乘除,最後計算加減)。由於地區不同,你的記憶中可能是BODMAS或PEMDAS。

用Groovy進行數學運算比Java簡單。如果你想瞭解底層細節,可以訪問http://groovy.codehaus.org/Groovy+Math,那裡有所有的細節信息。

接下來我們學習Groovy如何處理變量和作用域。因為Groovy的動態性和執行腳本的能力,它在這方面的語義規則和Java稍有不同。

8.2.3 變量、動態與靜態類型、作用域

因為Groovy是一種能作為腳本語言的動態語言,所以你要清楚動態類型和靜態類型一些細微差別,還需要瞭解Groovy如何限定變量的作用域。

提示 如果你意在讓Groovy代碼與Java互操作,它也能在可能的情況下使用靜態類型,因為它簡化了類型重載和調度機制。

首先你要理解Groovy動態類型和靜態類型的差別。

1. 動態類型與靜態類型

Groovy是動態語言,所以不必指定變量的類型,變量的類型是在聲明(或返回)時確定的。比如說,你可以把一個Date賦值給變量x,然後緊接著再用不同的類型給x賦值。

x = new Date;
x = 1;
  

用動態類型能讓代碼更簡潔(忽略顯而易見的類型信息),反饋更快,並且很靈活,可以在一個變量上賦予不同類型的對象來完成工作。對於那些想對自己使用的類型更有把握的人,Groovy也確實支持靜態類型。比如:

Date x = new Date;
  

如果聲明了靜態類型變量,在用不正確的類型值對它賦值時,Groovy能檢查出來。比如:

Exception thrown
org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast
     object \'Thu Oct 13 12:58:28 BST 2011\' with class \'java.util.Date\' to
     class \'double\'
...
  

在Groovy控制台中運行下面的代碼,就可以重現上面的輸出。

double y = -3.1499392;
y = new Date;
  

如你所料,Data類型的值不能賦給double變量。Groovy中的動態和靜態類型都討論到了,那作用域呢?

2. Groovy中的作用域

對於Groovy裡的類,其作用域跟Java一樣,類、方法、循環作用域的變量,它們的作用域都跟你想的一樣。但涉及Groovy腳本時,這個話題就變得比較有意思了。

提示 記住,作為腳本的Groovy代碼不在平常的類和方法結構中。8.1.1節已經給過一個例子了。

簡單說,Groovy腳本有兩種作用域。

  • 綁定域,綁定域是腳本的全局作用域。

  • 本地域,本地域就是變量的作用域局限於聲明它們的代碼塊。對於在腳本代碼塊內聲明的變量(比如在腳本的頂部),如果是定義過的變量,其作用域就是定義它的本地域。

能在腳本中使用全局變量可以極大提高代碼的靈活性。它和Java中類範圍內的變量有點像。定義變量是指被聲明為靜態類型,或用特殊的def關鍵字定義的變量(表明它是未確定類型的定義變量)。

在腳本中聲明的方法訪問不了本地域。如果你調用一個試圖引用本地域中的變量的方法,會提示類似下面的錯誤消息:

groovy.lang.MissingPropertyException: No such property: hello for class:
     listing_8_2
...
  

下面是產生該異常的代碼,說明了作用域的這個問題。

String hello = \"Hello!\";
void checkHello
{
  System.out.println(hello);
}
checkHello;
  

如果用hello = \"Hello!\"換掉上面代碼裡的第一行,這個方法可以成功輸出「Hello」。因為hello不再定義為String,它現在的作用域是 綁定域。

除了編寫Groovy腳本時的這些差異,動態和靜態類型、作用域、變量聲明都跟你想的完全一樣。接下來我們去看看Groovy內置的集合(列表和映射)支持。

8.2.4 列表和映射語法

Groovy把列表和映射(包括集合)結構當做語言中的一等公民對待,所以沒必要像Java那樣顯式聲明ListMap結構。也就是說,Groovy中的列表和映射在底層是由Java ArrayListLinkedHashMap實現的。

使用Groovy語法最大的優勢在於可以省掉很多套路化的代碼,讓代碼更簡潔,但絲毫不影響可讀性。

Groovy用方括號指定和使用列表結構(是不是想起了Java中的原生數組語法)。下面的代碼展示了如何引用第一個元素(Java),獲取列表大小(4),以及將列表設置為空

jvmLanguages = [\"Java\", \"Groovy\", \"Scala\", \"Clojure\"];
println(jvmLanguages[0]);
println(jvmLanguages.size);
jvmLanguages = ;
println(jvmLanguages);
  

看,Groovy將列表作為一等公民處理要比用java.util.List及其實現類的代碼輕量得多。

因為Groovy是動態類型語言,我們可以把不同類型的值保存在列表(或映射)中,所以下面的代碼也是正確的:

jvmLanguages = [\"Java\", 2, \"Scala\", new Date];
  

Groovy處理映射也跟這差不多,用符號,並用冒號(:)來分開鍵/值對。以映射.鍵的方式引用映射中的值。下面的代碼通過相應的操作展示了這些功能:

  • 引用鍵\"Java\"的值100
  • 引用鍵\"Clojure\"的值\"N/A\"
  • 將鍵\"Clojure\"的值變成75
  • 將映射設為空([:])。

    languageRatings = [Java:100, Groovy:99, Clojure:\"N/A\"];
    println(languageRatings[\"Java\"]);
    println(languageRatings.Clojure);
    languageRatings[\"Clojure\"] = 75;
    println(languageRatings[\"Clojure\"]);
    languageRatings = [:];
    println languageRatings;
      

提示 你有沒有注意到映射裡的鍵是不帶引號的字符串?為了讓代碼更簡潔,Groovy對這個語法也做了調整,映射鍵的引號可用可不用。

這種寫法很直觀,用起來也舒服。Groovy把對映射和列表內置支持的概念更進了一步。

還有一些語法技巧,比如引用集合中一定範圍內的元素,甚至可以用負索引引用最後一個元素。下面的代碼引用了列表中的前三個元素([Java, Groovy, Scala])和最後一個元素(Clojure)。

jvmLanguages = [\"Java\", \"Groovy\", \"Scala\", \"Clojure\"];
println(jvmLanguages[0..2]);
println(jvmLanguages[-1]);
  

現在,我們已經瞭解了Groovy的一些基本語法和語義。但在真正使用Groovy之前,還需要學習更多內容。下一節會更深入地探討Groovy的語法和語義,重點講解Java開發人員學習Groovy過程中那些「難纏的內容」。