首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《Java程序员修炼之道》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 = 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,假如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 * 2String crawling = "${name} is crawling ${dist} feet!"  

其中的表达式计算后被转到可以调用toStringObject上,或是函数字面值上。(请参见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正则表达式语法

方法描述及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 =~ 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。产生personXML记录的代码如代码清单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表达式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) //❷读取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多语言程序员。