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 = 18println "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 * 2String crawling = "${name} is crawling ${dist} feet!"
其中的表达式计算后被转到可以调用toString
的Object
上,或是函数字面值上。(请参见http://groovy.codehaus.org/Strings+and+GString了解关于函数字面值和GString
规则的细节。)
警告
GString
的底层并不是 Java中的String
!尤其不应该把GString
作为映射中的键,或者比较它们是否相等。结果是不可预料的!
Groovy中另一个有点儿用的结构是三引号String
或三引号GString
,它们可以在源码中定义跨行字符串。
"""This GStringwraps 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 König、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 =~ patternif (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 StringWriterdef 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) //❷读取XMLPerson p = new Person(id: [email protected], 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多语言程序员。