Groovy具備一些Java沒有的語言特性,起碼Java 7還沒有。優秀的Java開發人員就是在這些問題上需要向新語言求助,希望能以更優雅的方式解決它們。本節就探索幾個這樣的特性,包括:
GroovyBean,更簡單的bean;
用操作符
?.
實現null
對象的安全訪問;貓王1操作符(Elvis operator),更短的
if
/else
結構;Groovy字符串,更強的字符串抽像;
函數字面值(即閉包),把函數當做值傳遞;
對正則表達式的本地支持;
更簡單的XML處理。
1 Elvis Aron Presley(1935—1977),美國搖滾樂史上影響力最大的歌手,有搖滾樂之王的譽稱。——譯者注
我們會從GroovyBean開始,因為Groovy代碼中經常見到它們。作為一名Java開發人員,你可能有點兒疑心,因為按JavaBean的標準來衡量的話,它們不太完整。但請你放心,GroovyBean很完整,分毫不差,並且用起來更方便。
8.4.1 GroovyBean
GroovyBean很像JavaBean,不過省略了顯式聲明的獲取和設置方法,提供了自動構造方法,並允許你用點號(.
)引用成員變量。如果需要把某個獲取方法或設置方法設為private
,或者希望改變默認的行為,可以顯式聲明那個方法,並按你的想法修改它。自動構造方法只是一個用來構造GroovyBean、傳入與GroovyBean的成員變量對應的參數的映射。
不論是不辭勞苦自己輸入獲取方法和設置方法,還是用IDE生成,所有這些都省去了我們處理JavaBean時所要編寫的大量套路化代碼。
我們以一個角色扮演遊戲(RPG)1里的Character
類為例來看一下GroovyBean是如何工作的。代碼清單8-4會輸出STR[18], WIS[15]
,這是代表GroovyBean力量和智慧的成員變量。
1 這裡大力推薦一下PCGen(http://pcgen.sf.net),對於RPG粉來說真是個非常好的開源項目。
代碼清單8-4 探索GroovyBean
class Character
{
private int strength
private int wisdom
}
def pc = new Character(strength: 10, wisdom: 15)
pc.strength = 18
println "STR [" + pc.strength + "] WIS [" + pc.wisdom + "]"
它的行為跟Java裡的JavaBean非常相似(封裝性得以保留),而語法更精簡。
提示 可以用
@Immutable
註解使GroovyBean不可變(意思是它的狀態不可修改)。這對於傳遞線程安全的數據結構很有用,在並發代碼中用起來更安全。第10章討論閉包時我們還會進一步討論不可變數據結構的概念。
接下來我們會轉向Groovy檢查null
引用的能力。這會進一步減少套路化代碼,以便你可以更快地把想法變成原型。
8.4.2 安全解引用操作符
NullPointerException
1(NPE)是所有Java開發人員都揮之不去的夢魘(很不幸)。為了避開NPE,Java程序員通常都會在引用對像之前檢查一下它是否為null
,特別是在他們不能保證所處理的對象不是null
的情況下。如果你準備在Groovy中延續那種開發風格,為了遍歷一個Person
對像列表,最終編寫的代碼可能像下面這樣(只是輸出「Gweneth」)。
List<Person> people = [null, new Person(name:"Gweneth")]
for (Person person: people) {
if (person != null) {
println person.getName
}
}
1 Java最大的憾事就是沒據實把這個叫做NullReferenceException
,本書的一位作者對此一直頗多怨言!
Groovy引入了安全解引用運算符,用?.
符號幫你去掉一些套路化的「如果對像為null
」檢查代碼。在使用這個符號時,Groovy引入了一個特殊的null
結構,表示「什麼也不做」,而不是真的引用null
。
在Groovy中,可以用安全解引用語法重寫上面的代碼:
people = [null, new Person(name:"Gweneth")]
for (Person person: people) {
println person?.name
}
Groovy函數也支持這種安全解引用,所以Groovy的默認集合方法(比如max
方法),能自動處理好null
引用。
接下來是貓王操作符,看起來和安全解引用差不多,但它是用來減少某些if
/else
結構中的代碼的。
8.4.3 貓王操作符
用貓王操作符(?:
)可以把帶有默認值的if
/else
結構寫得極其短小。為什麼叫貓王?因為這個符號看起來明顯很像貓王鼎盛時期梳的大背頭[1]。用貓王操作符不用檢查null
,也不用重複變量。
1 本書的作者都鄭重聲明,我們根本不知道貓王在鼎盛時期長什麼樣。我們真沒那麼老,不開玩笑!
假設你要檢查王牌大賤諜是不是活躍的偵探。在Java中可能要用三元操作符:
String agentStatus = "Active";
String status = agentStatus != null ? agentStatus : "Inactive";
Groovy能縮短這個語句,是因為它能在需要時將類型強制轉換為boolean
,比如if
語句的條件判斷。在前面的代碼中,Groovy把String
轉換為boolean
,假如String
是null
,它會被轉換成Boolean
值false
,所以可以省略null
檢查。因而前面的代碼可以寫成這樣:
String agentStatus = "Active"
String status = agentStatus ? agentStatus : "Inactive"
但這樣還是要重複agentStatus
變量,Groovy可以讓我們不再重複輸入。用貓王操作符可以去掉重複的變量名:
String agentStatus = "Active"
String status = agentStatus ?: "Inactive"
第二個agentStatus
沒了,代碼更簡潔了。
好了,現在該去看看Groovy字符串了,看看它們跟Java常規String
有什麼不同。
8.4.4 增強型字符串
Groovy有一個String
類的擴展類GString
,它比Java中標準的String
強,也更靈活。
儘管雙引號也有效,但按照慣例,普通字符串是用開閉兩個單引號定義的。比如:
String ordinaryString = 'ordinary string'
String ordinaryString2 = "ordinary string 2"
而GString
必須用雙引號定義。對於開發人員來說,使用它最大的好處是可以包含可在運行時計算的表達式(用${}
)。如果GString
隨後被轉為普通字符串(比如傳給了println
),GString
中的表達式都會被替換為其計算結果。比如:
String name = 'Gweneth'
def dist = 3 * 2
String crawling = "${name} is crawling ${dist} feet!"
其中的表達式計算後被轉到可以調用toString
的Object
上,或是函數字面值上。(請參見http://groovy.codehaus.org/Strings+and+GString瞭解關於函數字面值和GString
規則的細節。)
警告
GString
的底層並不是 Java中的String
!尤其不應該把GString
作為映射中的鍵,或者比較它們是否相等。結果是不可預料的!
Groovy中另一個有點兒用的結構是三引號String
或三引號GString
,它們可以在源碼中定義跨行字符串。
"""This GString
wraps over two lines!"""
接下來我們要向函數字面值進軍了。由於最近幾年業內興起了對函數式語言的興趣,這個編程技巧也成了一個熱門話題。要弄懂函數字面值,可能需要動動腦筋。如果你沒用過,也就是說如果這是你第一次用,也許你現在就該先起身將公爵杯加滿自己喜歡的飲品。
8.4.5 函數字面值
函數字面值表示一個可以當做值傳遞的代碼塊,也可以像操作任何值一樣操作。可以當參數傳給方法,可以給變量賦值,等等。這個語言特性已經成為Java社區的討論熱點,但對於Groovy程序員來說,它們是標配的工具。
舉例說明向來都是學習新概念的最好方法,我們先來看幾個例子吧!
假設我們有一個普通的靜態方法,要構建一個String
來向作者或讀者問好。我們用常規方式從這個類的外部調用該方法,如代碼清單8-5所示:
代碼清單8-5 一個簡單的靜態函數
class StringUtils
{
static String sayHello(String name) //靜態方法聲明
{
if (name == "Martijn" || name == "Ben")
"Hello author " + name + "!"
else
"Hello reader " + name + "!"
}
}
println StringUtils.sayHello("Bob"); //調用者
有了函數字面值,你不用方法或類結構也可以實現同樣的功能,只要把代碼放在函數字面值裡。而函數字面值又可以賦值給一個變量,從而可以被傳遞和執行。
代碼清單8-6把函數字面值賦值給sayHello
,傳入參數"Martijn"
,並最終輸出「Hello author Martijn!」。
代碼清單8-6 使用簡單的函數字面值
def sayHello = //函數字面值賦值
{
name -> //❶變量與處理邏輯分開
if (name == "Martijn" || name == "Ben")
"Hello author " + name + "!"
else
"Hello reader " + name + "!"
}
println(sayHello("Martijn")) //輸出結果
注意函數字面值開始處的{
。把傳入函數字面值的參數跟處理邏輯分開的箭頭(->
)❶。最後是函數字面值結束處的}
。
在代碼清單8-6中,函數字面值的定義方式非常像方法的定義方式。因此你可能在想:「它們看起來也不是特別有用!」只有開始用它們創作(用函數方式思考)時,你才能真正發現它們的好,比如說跟Groovy對集合的內置支持結合起來之後,函數字面值會特別強大。
8.4.6 內置的集合操作
Groovy有幾個可以用於集合(列表和映射)的內置方法。這種在語言層面對集合的支持,跟函數結合在一起,可以極大減少程序員在Java中必寫的那些套路化代碼;並且代碼仍然很容易看懂,不影響維護。
表8-1是一些使用了函數字面值的內置函數。
表8-1 Groovy中的部分集合函數
each
遍歷集合,對其中的每一項應用函數字面值
collect
收集在集合中每一項上應用函數字面值的返回結果(相當於其他語言map/reduce中的map函數)
inject
用函數字面值處理集合併構建返回值(相當於其他語言裡map/reduce中的reduce函數)
findAll
找到集合中所有與函數字面值匹配的元素
max
返回集合中的最大值
min
返回集合中的最小值
Java編程過程中遍歷集合,並對其中每個對象執行某種操作是很常見的任務。比如說,如果你想在Java 7中輸出電影名稱,很可能會寫出如代碼清單8-7所示的代碼:
代碼清單8-7 在Java 7中輸出一個集合
List<String> movieTitles = new ArrayList<>;
movieTitles.add("Seven");
movieTitles.add("Snow White");
movieTitles.add("Die Hard");
for (String movieTitle : movieTitles)
{
System.out.println(movieTitle);
}
1 不,我們可不會告訴你誰喜歡《白雪公主》(反正不是我倆)!
Java中有幫你少寫代碼的技巧,但不管怎樣都要用某種循環結構手工遍歷電影名稱的List
。
在Groovy裡可以用內置的集合遍歷功能(each
函數),並且函數字面值可以減少大量你需要自己編寫的代碼。此外,這樣還能反轉列表和所要執行的算法之間的關係。不再是把集合傳遞到方法中,而是把方法傳入到集合中!
下面的代碼和代碼清單8-7所做的工作完全一樣,但只有短短的兩行,很容易讀懂:
movieTitles = ["Seven", "SnowWhite", "Die Hard"]
movieTitles.each({x -> println x})
實際上,如果使用隱含的it
變量,這段代碼還可以變得更精簡,it
變量可以用在單參的函數字面值中,代碼如下所示2:
movieTitles = ["Seven", "SnowWhite", "Die Hard"]
movieTitles.each({println it})
2 Groovy高手會說實際上還可以簡化,一行足矣!
看,這段代碼簡潔易讀,並且效果和Java 7那個版本一樣。
提示 只能介紹這麼多了,如果你想研究更多例子,推薦你到Groovy的網站上去看看與集合相關的內容(http://groovy.codehaus.org/JN1015-Collections),或者讀讀Dierk Konig、Guillaume Laforge、Paul King、Jon Skeet和Hamlet D'Arcy合著的Groovy in Action, second edition(Manning, 2012),這是一本相當不錯的書。
下一個語言特性是Groovy內置的正則表達式支持,你可能要花點兒時間才能熟悉,所以藉著咖啡勁兒,我們趕緊來看看吧!
8.4.7 對正則表達式的內置支持
Groovy把正則表達式當做語言的一部分,所以用Groovy處理文本要比Java簡單得多。表8-2中是Groovy可用的正則表達式語法,以及Java與之對應的東西。
表8-2 Groovy正則表達式語法
Pattern
對像)
=~創建一個匹配器(創建一個Java Matcher
對像)
==~計算字符串(相當於在Pattern上調用Java match
方法)
假設你從一個硬件上收到了一些日誌數據,要部分匹配其中一些錯誤日誌。比如查找模式1010
的實例,然後再找0101
。在Java 7中,實現代碼可能如下所示。
Pattern pattern = Pattern.compile("1010");
String input = "1010";
Matcher matcher = pattern.matcher(input);
if (input.matches("1010"))
{
input = matcher.replaceFirst("0101");
System.out.println(input);
}
在Groovy中,每行代碼都變短了,因為Pattern
和Matcher
對象是內置在語言中的。當然,輸出(0101
)還和原來一樣,請看代碼。
def pattern = /1010/
def input = "1010"
def matcher = input =~ pattern
if (input ==~ pattern)
{
input = matcher.replaceFirst("0101")
println input
}
Groovy支持完整的正則表達式語義,所採用的方式和Java一樣,所以你熟悉的那種靈活性還在。
正則表達式跟函數字面值結合得也很好。比如分析String
得到一個人的名字和年齡,並輸出詳細信息。
("Hazel 1" =~ /(\w+) (\d+)/).each {full, name, age
-> println "$name is $age years old."}
或許你應該借這個機會稍微放鬆一下,接下來我們馬上就要探索一項完全不同的技術:XML處理。
8.4.8 簡單的XML處理
Groovy有構建器的概念,用Groovy原生語法可以處理任何樹型結構的數據。包括HTML、XML和JSON。Groovy理解開發人員想輕鬆處理這種數據的需求,所以提供了開箱即用的構建器。
XML:一種被濫用的語言
XML是一種卓越、詳細的數據交換語言,但現在已經變得如洪水猛獸一般了。為什麼呢?因為軟件開發人員已經把XML當成編程語言來用了,可它不是圖靈完備1的語言,所以它不適合幹這些事。希望XML能在你的項目中得其所哉,只是用來交換數據。
1 對於一種語言來說,如果是圖靈完備的,那它至少必須能做條件分支判斷,並能修改內存數據。
本節重點是XML,一種常用的交換數據格式。儘管Java語言的核心(通過JAXB和JAXP)以及浩浩蕩蕩的第三方類庫(XStream、Xerces、Xalan等)組成了龐大的XML處理大軍,但選哪個方案經常讓人難以抉擇,並且採用相應方案的Java代碼會變得非常冗長。
本節會帶你用Groovy創建XML,並告訴你如何把XML解析為GroovyBean。
1. 創建XML
用Groovy構建XML文檔非常簡單,比如person
:
<person id='2'>
<name>Gweneth</name>
<age>1</age>
</person>
Groovy能用內置的MarkupBuilder
產生這個XML。產生person
XML記錄的代碼如代碼清單8-8所示:
代碼清單8-8 產生簡單的XML
def writer = new StringWriter
def xml = new groovy.xml.MarkupBuilder(writer)
xml.person(id:2) {
name 'Gweneth'
age 1
}
println writer.toString
注意看person
的起始元素(屬性id
設置為2)創建起來多麼簡單,根本不用定義Person
對象。Groovy不會強迫你顯式地弄一個GroovyBean
來支撐XML的創建,再一次節省了時間和精力。
代碼清單8-8中的例子相當簡單。你可以多做些試驗,把輸出類型StringWriter
改掉,並且可以嘗試用不同的構建器,比如groovy.json.JsonBuilder
,即刻創建JSON2。在處理更複雜的XML結構時,命名空間和其他特定構造的處理上也有額外的輔助方法。
2 關於這一問題,Dustin在他的博客Inspired by Actual Events(http://marxsoftware.blogspot.com/)上有一篇很棒的文章,標題是「Groovy 1.8 Introduces Groovy to JSON」。
你可能還希望執行反向操作,讀取XML並把它解析成GroovyBean
。
2. 解析XML
Groovy有幾種解析XML輸入的辦法。表8-3列出了其中三個方法,這是從Groovy的官方文檔(http://docs.codehaus.org/display/GROOVY/Processing+XML)中拿過來的。
表8-3 Groovy XML解析技術
XMLParser
支持XML文檔的GPath表達式
XMLSlurper
跟XMLParser
類似,但以懶加載的方式工作
DOMCategory
用一些語法支持DOM的底層解析
這三個用起來都很簡單,但這一節我們主要關心XMLParser
的用法。
注意 GPath是一種表達式語言。Groovy文檔(http://groovy.codehaus.org/GPath)中有它的全部內容。
我們把代碼清單8-8中產生的那個表示「Gweneth」(人名)的XML拿過來,並把它解析到一個GroovyBean Person
中,如代碼清單8-9所示。
代碼清單8-9 用XMLParser解析XML
class XmlExample {
static def PERSON =
"""
<person id='2'>
<name>Gweneth</name>
<age>1</age>
</person>
"""
} //❶XML作為Groovy源碼
class Person {def id; def name; def age} //Groovy中的Person定義
def xmlPerson = new XmlParser.
parseText(XmlExample.PERSON) //❷讀取XML
Person p = new Person(id: xmlPerson.@id,
name: xmlPerson.name.text,
age: xmlPerson.age.text) //❸填入GroovyBean Person中
println "${p.id}, ${p.name}, ${p.age}"
我們一開始抄了點兒近路,把XML文檔直接放在代碼中了,這樣它就會出現在CLASSPATH中
❶。真正的第一步是用XMLParser
中的parseText
方法讀取XML數據❷。然後創建新的Person
對象,給它賦值❸,最後輸出Person
,以便你能用肉眼檢查一下。
我們對Groovy的介紹到此就完成了。現在,你可能覺得心裡癢癢的,想在自己的Java項目裡使用一些Groovy特性!下一節,我們會帶你看看Java如何跟Groovy互操作。由此你將邁出作為優秀Java開發者的重要一步:成為一名JVM多語言程序員。