讀古今文學網 > Java程序員修煉之道 > 8.4 Java不具備的Groovy特性 >

8.4 Java不具備的Groovy特性

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,假如Stringnull,它會被轉換成Booleanfalse,所以可以省略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!"
  

其中的表達式計算後被轉到可以調用toStringObject上,或是函數字面值上。(請參見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正則表達式語法

方法描述及Java中的對等物 ~創建一個模式(創建一個編譯的Java 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中,每行代碼都變短了,因為PatternMatcher對象是內置在語言中的。當然,輸出(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。產生personXML記錄的代碼如代碼清單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表達式 XMLSlurperXMLParser類似,但以懶加載的方式工作 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多語言程序員。