Groovy食谱: 第3章 Groovy初学者

Groovy是一种对Java的补充,提供了自动装箱、可选数据类型声明、操作符重载、可选的异常处理等特性,简化了编程。Groovy中的字符串支持嵌入表达式,列表和映射的快捷方式使得操作更加方便。此外,Groovy还引入了闭包和范围,允许更灵活的代码编写。这些特性减少了代码的冗余,提高了可读性和效率。
摘要由CSDN通过智能技术生成

第3章 Groovy初学者

Groovy是对Java的补充、扩充,在某些情况下,它还提供了非常必要的改进。(毕竟,Java早在1995年就发布了。那是软件时代的前寒武纪,不是吗?) 例如,Java中需要的一些东西在Groovy中是可选的:分号、数据类型,甚至异常处理。默认情况下,Groovy自动包含的包比Java多得多。 Groovy向现有类(如String、List和Map)添加了新的方便方法。所有这些操作都是为了消除历史上减慢Java开发过程的一些减速带。

Groovy最有趣的地方是,您一直在编写它,甚至没有意识到它。Java在99%的情况下都是有效的Groovy—只需将.java文件重命名为.groovy,就可以运行了。(参见第69页的第4章,Java和Groovy集成,了解少数几种使Java不能成为100%有效的Groovy的边缘情况。)Groovy是Java的一个超集。它绝不意味着要取代Java。事实上,如果没有Java, Groovy就不会存在。Groovy旨在成为比Java更好的Java,同时始终支持您的遗留代码库。

但是Groovy不仅仅改进了现有的语言。Groovy引入了新的类,如ClosureRangeGString。Groovy引入了安全解引用的概念,以避免冗长的空检查块。Groovy提供了一个新的特殊的多行字符串变量。总的来说,Groovy以一种积极的方式“拥抱和扩展”Java。继续读下去,看看如果Java是在21世纪编写的,它会是什么样子。

3.1 自动导入

import java.lang.*;
import java.util.*;
import java.net.*;
import java.io.*;
import java.math.BigInteger;
import java.math.BigDecimal;
import groovy.lang.*;
import groovy.util.*;

Java自动为您导入java.lang包。 这意味着您可以使用诸如String和Integer之类,并调用System.out.println(),而不必在每个Java文件的顶部键入 import java.lang.*

在Groovy中,您可以获得许多附加包。换句话说,您可以使用这些包中的类,而不必在文件的顶部显式地导入它们。这些自动导入的净效果是,在默认情况下,您可以使用更多的JDK和GDK。Java类及其Groovy增强功能,如List(第3.14节,第58页上的List快捷方式)、Map(第3.15节,第62页上的Map快捷方式)、File(第6章,第100页上的File Tricks)和URL(第9章,第152页上的Web服务),在您需要它们的时候就会出现。此外,常见的Groovy类,如XmlParseXmlSlurper(7.2节,理解XmlSlurper XmlParse和之间的区别,117页),Expando(10.9节,创建一个Expando, 194页),和ExpandoMetaClass(添加一个类的方法动态(ExpandoMetaClass), 190页)准备好了,等待你由于自动导入,Groovy并代表你。

3.2 可选的分号

msg = "Hello"
msg += " World"; msg += "!";
println msg;
===> "Hello World!"

在Groovy中,分号是完全可选的。如果同一行有许多语句,则必须使用它们。否则,在一个语句的行尾使用它们现在是一种风格上的决定,而不是编译器的要求。

当然,这意味着我们应该为下一场大规模的技术圣战做好准备。“呵,分号,分号!你为什么是分号?”

偷偷走向DSL

def list = []
list.add("Groovy")
list.add "Groovy"
list << "Groovy"

这三个语句都是等效的。 每个都将Groovy一词添加到列表中。 第一种使用传统的Java的add()方法。 第二个调用相同的方法,只是不带括号。 第三种方法使用运算符重载(如第50页第3.7节“运算符重载”中讨论的那样)。 <<操作符在幕后调用add()方法。您是否喜欢一种语法而不喜欢其他语法,这是个人喜好的问题。在每种情况下,Groovy都试图使您的代码尽可能具有表现力和易于阅读,同时仍然保留一些实际执行的代码。

使用动态语言(如Groovy)的一个好处是,它使创建特定于领域的语言(DSLs).∗变得很容易。特性,比如可选圆括号(第3.3节,后面一页是可选圆括号)和可选分号(第3.2节,前面一页是可选分号),为开发人员提供了工具,使编程变得不那么像编程。DSL可以看作是“可执行伪代码”。你也可以把它看作是一种允许非程序员做简单编程任务的方式。

def shoppingList = []
def add = shoppingList.&add
def remove = shoppingList.&remove
add "Milk"
add "Bread"
add "Beer"
remove "Beer"
add "Apple Juice"
print shoppingList

除了省略圆括号和分号之外,这个简单的例子还使用了方法指针(第10.7节,在第193页创建一个方法指针)来进一步简化语法。很快,您就有了一些与编写源代码完全不同的东西。添加“牛奶”、删除“啤酒”和打印购物清单都感觉非常自然,即使对于非程序员也是如此。下一页继续。


(DSLs).∗ : http://en.wikipedia.org/wiki/Domain-specific_programming_language

偷偷走向DSL(续)

将此与Java替代方法进行比较:“不要忘记在每一行的末尾都包括分号。分号。 就像“ 3:00”中小时和分钟之间的内容一样,只在逗号顶部加一个点,而不是两个点。 您找到了它,它位于键盘上的L键旁边。 好,现在让我们继续
公共静态void main(String [] args)…”

DSL的最好之处在于,它们不仅为初学者和非程序员带来好处-简化源代码对于所有相关人员而言都是轻松的胜利。

我讨厌争论花括号应该放在哪里—如果对Kernighan和Ritchie足够好[^31],那么对我来说也足够好。 就我而言,文本编辑器之战[^32]的胜利者已经决定了。您可以使用emac—我有一个可行的替代方案。(虽然有人在我背后说我是旧恶习的受害者,但我不会用回应来美化那些恶毒的谣言。)

那么,当涉及到可选分号时,我们该怎么办呢?我个人不使用它们,坦白地说,我也不想念它们。我认为,如果它们不是真正必需的,那么它们只不过是视觉上的杂乱-一个残留的尾巴,它反映了Groovy的过去,而不是决定了它的未来。一旦您被DSL的bug所困扰(请参阅前一页的侧栏),就有机会去掉无法发音的符号,转而使用更像英语的编程风格,这是一个可喜的改变。(当然,我总是愿意你请我喝杯啤酒,试着让我明白我的错误。事先警告一下-可能要喝上几品脱才能说服我……)

3.3 可选的括号

println("Hello World!")
println "Hello World!"
===> "Hello World!"

在Groovy中,方法参数周围的括号是可选的。这通常用于简单的方法,如println。但是,如果方法没有参数,则必须仍然使用圆括号。例如:

def s = "Hello"
println s.toUpperCase()
===> HELLO

无参数方法需要圆括号,因为否则编译器将无法区分方法调用和第4.2节(第72页的getter和setter快捷语法)中讨论的简短getter/setter调用之间的区别。在使用Groovy一段时间之后,当您在代码中看到person.name时,您将知道它是调用person.getName()的Groovy快捷方式。

如何使无参数方法括号可选
当然,如果整个“无参数括号”要求确实让您彻夜难眠,那么有几种巧妙的方法可以解决此问题。 (不,我不建议“切换到Ruby”。)

第一个解决方案是创建一个看起来像getter的方法,即使它根本不是真正的getter。我不是一个骄傲的人——我已经知道在我的Pizza类上编写getDeliver()这样的方法,以便稍后调用Pizza .deliver。当然,这违反了神圣的“getter/setter”契约,这是所有新手Java开发人员都必须签署的契约,但是如果不偶尔违反这些规则,为什么还要制定规则呢?

另一个绕过这些讨厌的空括号的选项是创建一个方法指针,如第10.7节中讨论的,在第193页创建一个方法指针:

def pizza = new Pizza()
def deliver = pizza.&deliver()
deliver

何时使用括号,何时省略括号
既然您已经决定是否要使用分号,那么您将面临何时使用括号的难题。

我给你的建议和最高法院法官波特·斯图尔特的建议是一样的:当你看到它的时候你就会知道了。println "Hello"看起来不是比System.out.println("Hello")更好吗?我不能告诉你为什么——它就是这样。

但这并不意味着我总是避免括号。我可能用的比不用的多。如果我正在编写DSL(如第43页边栏中讨论的那样),我倾向于使用更少的括号。如果我正在编写更传统的Java/Groovy代码,我将更经常地使用它们。但是在一天结束的时候,我没有一个艰难而快速的决策过程,除了“在这个时候,去掉括号似乎是正确的做法。”

3.4 可选的返回语句

String getFullName(){
  return "${firstName} ${lastName}"
}

//equivalent code
String getFullName(){
  "${firstName} ${lastName}"
}

Groovy中方法的最后一行是一个隐式返回语句。我们可以显式地使用return语句,也可以安全地关闭它。

那么,为什么return语句是可选的呢?因为艾尔·戈尔说所有那些多余的不必要的打字是全球警告的第623个主要原因。“拯救按键,拯救地球”不仅仅是我当场想出的一个朗朗上口的口号。(事实上是这样,但你不同意它看起来像你会在《难以忽视的真相》中看到的东西吗?)

就像本章中所有其他可选内容一样,允许您省略return语句是为了减少编程语言的视觉噪音。在我看来,创建add(x,y){x + y}这样的方法是在简洁和可读性之间取得了恰当的平衡。如果你觉得它太简洁,那就不要用它。真的。没关系。

如果我需要过早地退出一个方法,我就会使用return语句。例如,我非常相信快速失败,所以在我的withdraw()方法中,会尽快返回“资金不足——稍后再试”。如果我在方法的早期使用return,我可能也会在最后一行使用它来实现视觉对称。另一方面,return并没有为快速的单行方法(如前一段中的add方法)增加多少清晰度。Groovy允许我有目的地编程,而不是让我屈服于编译器的同行压力。当我准备好时,我将使用return,而不是因为编译器让我这么做。

3.5 可选数据类型声明(鸭子类型)

//In scripts:
w = "Hello"
String x = "Hello"
println w.class
===> java.lang.String
println w.class == x.class
===> true
//In compiled classes:
def y = "Hello"
String z = "Hello"
println y.class
===> java.lang.String
println y.class == z.class
===> true

Groovy不会强制您显式地定义变量的类型。def name = "Jane"等价于String name = "Jane"——两者都是字符串。关键字def的意思是,“我不太关心这个变量是什么类型的,您也不应该太关心。请注意,在脚本和Groovy Shell中(与编译类相反),您可以更加随意,完全不使用def。实际上,在Groovy Shell中,应该去掉数据类型声明。(更多信息请参见第30页的侧栏。)

另一方面,Java是一种静态类型语言。这意味着当你声明每个变量时,你必须给它一个数据类型:

Duck mallard = new Mallard();

在这段代码中,您不能分辨Duck是类还是接口。(考虑 List List = new ArrayList()ArrayList List = new ArrayList()。)鸭子可能是野鸭的父类。也许它是一个定义鸭子行为的接口。如果编译器允许您将绿头鸭填充到鸭子形状的变量中,那么绿头鸭必须提供与鸭子相同的所有方法。无论绿头鸭是如何实现的,您都可以安全地说(至少可以说)绿头鸭是Duck类型的。

这个概念称为多态性-希腊语为“许多形状”。多态性是运行流行的依赖项注入(DI)框架(例如Spring,HiveMind和Google Guice)的动力。 这些DI引擎允许开发人员保持其类之间的松散耦合。 例如,如果您在整个代码中对对MySQL JDBC驱动程序的引用进行硬编码,则如果您以后决定切换到PostgreSQL,则必须执行广泛的搜索和替换任务。 另一方面,java.sql.Driver是一个接口。 您可以简单地对Driver接口进行编码,并允许Spring在运行时注入适当的JDBC驱动程序实现。

Groovy用Java编写,因此通过扩展,所有变量都具有特定的数据类型。 Groovy的不同之处在于,您不必在使用变量之前就明确声明它的数据类型。 在快捷脚本中,这意味着您只需编写w =“ Hello”。 您可以确定w确实是java.lang.String类型,不是吗? 使用groovyc编译Groovy时,如果要声明变量而不显式声明类型,则必须使用def关键字。

为什么这很重要? 这不仅是为您节省了一些宝贵的按键。 这很重要,因为它将Groovy从一种静态类型的语言转移到了一种动态类型的语言。 动态类型语言的对象在编译时不必满足接口的“合同”要求; 他们只需要在运行时正确响应方法调用即可。 (有关此示例,请参见第185页的第10.3节“检查字段的存在”和第190页的第10.5节“检查方法的存在”。)

def d = new Duck()

几本畅销Python书籍的作者Alex Martelli创造了duck typing[^35] 这个短语来描述动态类型语言。只要变量像鸭子一样“走路”和像鸭子一样“嘎嘎叫”,它就不必被正式声明为Duck类型——换句话说,它必须在运行时响应那些方法调用。

3.6 可选的异常处理

//in Groovy:
def reader = new FileReader("/foo.txt")

//in Java:
try{
  Reader reader = new FileReader("/foo.txt")
} catch(FileNotFoundException e){
  e.printStackTrace()
}

在Java中,有两种类型的异常:检查的和未检查的。已检查异常扩展java.lang.Exception。我们必须封装可能在try/catch块中抛出异常的方法。例如,如果传入不存在的文件名,FileReader构造函数将抛出FileNotFoundException。未检查异常扩展java.lang.Errorjava.lang.RuntimeException。 方法可能会引发NullPointerException,ClassCastException和IndexOutOfBoundsException之类的异常,但是编译器不需要您将它们包装在try/catch块中。 针对java.lang.Error的Javadoc说,我们不需要捕获此类异常,因为这些错误是不应该发生的异常情况。

尽管Java允许在已检查和未检查的异常之间进行微妙的区分是很不错的做法,但不幸的是,我们开发人员无法自行确定严重性级别。 如果FileReader构造函数引发了一个已检查的异常,并且您认为该异常不够重要,则编译器将尊重您的意见并拒绝编译您的代码。

$ javac TestFile.java
TestFile.java:6: unreported exception java.io.FileNotFoundException;
must be caught or declared to be thrown
Reader reader = new FileReader("/foo.txt");
1 error

但是,如果您只是在上一行中显式创建了文件,该怎么办? 上一次文件创建失败是什么时候? 是否有95%的可能性发生?5%?0.0005%? 它类似于SunSetException(每天发生的事情)还是SunJustExplodedException? 换句话说,是您期望发生的事情还是可能发生的事情(“永远不应该发生的异常情况”)?

如果您一直在写该文件,而现在只想读回内容怎么办? FileNotFoundException在这里是否有意义? 如果您试图获取操作系统上始终存在的目录的句柄,例如/etc/hostsc:\windows,该怎么办? 即使编译器具有最佳意图,一个简单的单行命令现在也需要六行代码。

更阴险的是,您认为catch块现在包含什么?如果您回答“什么都没有”、“我的IDE生成了什么”或“关闭那个愚蠢的编译器的最低限度”,那么您是正确的。

格伦·范德堡(Glenn Vanderburg)说:“错误的开发人员会把天堂和地球转移到错误的地方。”但是,良性疏忽呢?只接受您的IDE自动生成的代码(这很可能是带有todo标签的空块)?

如果我踢到你最喜欢的圣牛的小腿,我道歉。我很欣赏检查异常的意图, 但是一想到现在有多少空的catch块在生产环境中运行,有多少开发人员在常规实践中捕获异常,我就不寒而栗, 以及有多少异常被吃掉了,并且永远不会被重新抛出,因为它们的目的是为了让应用程序保持正常运行。

现在考虑有多少代码专门用于可怕的(但未选中)NullPointerException。我经常得到null值,但是编译器将其归类为“不应该发生的异常情况”。“显然,在已检查和未检查异常的意图和现实之间存在差距。

Groovy通过将所有已检查的异常转换为未检查的异常来解决这个问题。这一小步将返回异常严重程度的判断给开发人员。如果您运行的web服务经常从最终用户获得格式不正确的请求,您可能会选择显式地捕获NullPointerException,即使Java编译器不需要它。 如果您指的是一个不可能丢失的文件(例如: WEB-INF/web.xml)。您可以选择不捕获FileNotFoundException。 多亏了Groovy,“不应该发生的异常条件”的定义现在完全回到了您的控制之中。就像使用可选的逗号和括号一样,您的编程也是有目的的。捕获异常是因为您希望这样做,而不是编译器希望您这样做。

3.7 操作符重载

def d = new Date()
===> Sat Sep 01 13:14:20 MDT 2007

d.next()
===> Sun Sep 02 13:14:20 MDT 2007

(1..3).each{ println d++ }
===>
Sat Sep 01 13:14:20 MDT 2007
Sun Sep 02 13:14:20 MDT 2007
Mon Sep 03 13:14:20 MDT 2007

在离开Java语言很长一段时间之后,操作符重载在Groovy中仍然很活跃。正如您在本例中所看到的,++操作符在幕后调用next()方法。下面的列表显示了操作符和相应的方法调用:

操作符方法
a == b or a != ba.equals(b)
a + ba.plus(b)
a - ba.minus(b)
a * ba.multiply(b)
a / ba.div(b)
a % ba.mod(b)
a++ or ++aa.next()
a- - or - -aa.previous()
a & ba.and(b)
ab
a[b]a.getAt(b)
a[b] = ca.putAt(b,c)
a << ba.leftShift(b)
a >> ba.rightShift(b)
a < b or a > b or a <= b or a >= ba.compareTo(b)

这种语法糖出现在GDK[^37] (JDK的Groovy增强)中。例如,第58页的第3.14节“列表快捷方式”演示了添加到java.util.List中的一些方便操作符。您可以使用传统的Java方法(List.add("foo"))或新的Groovy方法(List << "foo")向列表添加项。

当然,您也可以将这些方法添加到您自己的类中。 在Groovy中 order.leftShift(item) 变成 order << item .

是否使用运算符重载取决于您,但是我必须承认,date + 7的感觉比date.roll(Calendar.DATE,7)更加自然。

3.8 安全解除引用(?)

def s = "Jane"
s.size()
===> 5
s = null
s.size()
Caught: java.lang.NullPointerException: Cannot invoke method size()
        on null object at CommandLine.run(CommandLine.groovy:2)
//notice that we can call size()
//without throwing a NullPointerException
//thanks to the safe dereferencing ? operator
s?.size()
===> null

Null引用可能会意外出现。 由于它们既常见又昂贵(在Java中引发异常会中止操作),因此许多Java程序员习惯于围绕潜在的空情况进行防御性编程,例如:

if(s != null){
  s.doSomething();
}

如果接收到Null引用并不像编译器希望的那样灾难性,那么这将很繁琐(且冗长)。 如果您想忽略NullPointerException并以静默方式进行操作,Groovy提供了一种快捷方式。 在任何可能为空的对象引用的末尾添加一个问号,Groovy会在后台为您将调用包装在try/catch块中。

s?.doSomething()

此安全解除引用可以链接到任何深度。 假设您有一个Person类,它有Address类和PhoneNumber类。 您可以安全地一直追溯到电话号码,而不必担心为每个单独的潜在NullPointerException捕获信息。

//in Java:
if(person != null && person.getAddress() != null
          && person.getAddress().getPhoneNumber() != null ){
  System.out.println(person.getAddress().getPhoneNumber());
}
else{
  System.out.println("");
}

//in Groovy:
println person?.address?.phoneNumber

3.9 自动装箱

def i = 2
println i.class
===> java.lang.Integer

def d = 2.2
println d.class
===> java.math.BigDecimal

自动装箱有助于克服Java语言的特殊性:Java是面向对象的。 Java提供原始数据类型(int,float,double)以及对象(Integer,Float,Double)。 在1995年,这是一个合理的让步。 使用基本类型提高速度; 使用对象是为了方便开发人员。 在发布Java 5时,Sun添加了自动装箱(透明地将原语提升为大写字母对象中),以帮助消除这种历史上的奇怪现象。Sun并没有消除原始/对象的划分;它只是让它不那么容易显现。

Groovy使Java 5自动装箱又迈出了一步-它可以快速自动装箱所有东西。 这意味着您可以执行有趣的任务,例如在Java开发人员看来像原始的对象上调用方法:

2.class
===> class java.lang.Integer

2.toFloat()
===> 2.0

3.times{println "Hi"}
Hi
Hi
Hi

即使显式地将变量转换为原始类型,仍然会得到一个对象。在Groovy中,一切都是对象。一切。就Groovy而言,原始类型已经不存在了。

float f = (float) 2.2F
f.class
===> class java.lang.Float

如果调用一个Java方法,而该方法需要的是原始类型而不是对象,情况又会如何呢?无需担心—groovy根据需要解箱(unboxes)这些值。如果你想要更精确的控制,你可以使用as关键字:

javaClass.javaMethod(totalCost as double)

如果显式地将数字转换为浮点数或双精度浮点数,它将自动装箱为浮点数或双精度浮点数。如果你只输入一个小数点后的数字,它会自动装箱为BigDecimal。这是为什么呢?它主要是为了避免Java中可怕的“浮点算术”问题:

//In Java:
public class PiggyBank{
public static void main(String[] args){
  double sum = 0.0d;
  for(int i = 0; i < 10; i++){
    sum += 0.1d;
  }
  System.out.println(sum);
  }
}

$ java PiggyBank
===> 0.9999999999999999

假设你连续十天把一枚10美分的硬币放在你的储蓄罐里。根据Java的说法,你最终得到的是一美元,还是一种渐进地接近一美元的东西,却从来没有真正得到它?

约书亚•布洛赫(Joshua Bloch)在他的开创性著作《有效的Java》(Effective Java)中有整整一节专门讨论了这一点。在149页,项目31的标题说明了一切:“如果需要确切的答案,避免浮点数和双精度数。Groovy如何处理相同的问题?

//In Groovy:
def sum = 0
10.times{ sum += 0.1}
println sum
===> 1.0

用于java.math的Javadoc。BigDecimal指出,它最适合用于“不可变的、任意精度带符号的小数”。BigDecimal类让用户完全控制舍入行为。“最小意外原则表明1.1 + 1.1应该返回2.2,10 * 0.1应该等于1.0。BigDecimal(和Groovy)提供了您期望的结果。

3.10 Groovy的True

//true
if(1) // any non-zero value is true
if(-1)
if(!null) // any non-null value is true
if("John") // any non-empty string is true

Map family = [dad:"John", mom:"Jane"]
if(family) // true since the map is populated

String[] sa = new String[1]
if(sa) // true since the array length is greater than 0

StringBuffer sb = new StringBuffer()
sb.append("Hi")
if(sb) // true since the StringBuffer is populated

//false
if(0) // zero is false
if(null) // null is false
if("") // empty strings are false

Map family = [:]
if(family) // false since the map is empty

String[] sa = new String[0]
if(sa) // false since the array is zero length

StringBuffer sb = new StringBuffer()
if(sb) // false since the StringBuffer is empty

"Groovy truth"是Groovy语言中评估为true的简写。 在Java中,唯一评估为true的东西就是true。 这会导致很多无关的输入。 例如,如果您尝试引入Java中的命令行参数,则必须执行以下操作:

//in Java:
if(args != null && args.length > 0){
  File dir = new File(args[0]);
} else{
  System.out.println("Usage: ListDir /some/dir/name" );
}

当然,您只需编写File dir = new File(args [0])并希望取得最佳效果。 但是,如果您的用户提供的参数数量不正确怎么办? 如果他们键入java ListDir而不是java ListDir /tmp,该怎么办? 您希望他们看到哪个错误?

//default message:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
at ListDir.main(ListDir.java:6)
//your custom error message:
Usage: ListDir /some/dir/name

多亏了Groovy truth,同样的错误捕获代码块可以被缩短为:

//in Groovy:
if(args){
  dir = new File(args[0])
}
else{
  println "Usage: ListDir /some/dir/name"
}

0、NULL和""(空字符串)的值都为false。这意味着在处理来自用户的输入时,简单的if(args)捕获了所有最可能要避免的事情。

3.11 嵌入引号

def s1 = 'My name is "Jane"'
def s2 = "My name is 'Jane'"
def s3 = "My name is \"Jane\""

Groovy向Java字符串添加了一些不错的新技巧。在Java中,一个单引号用于表示一个char基元。在Groovy中,我们可以使用单引号来包围字符串。这意味着我们可以使用单引号来保存包含双引号的字符串,而不必转义它们。当然,包含嵌入单引号的双引号字符串也是如此。使用退格转义字符在两种语言中是相同的。

3.12 heredoc(三重引号)

String s = """This is a
multi-line String.
"You don't need to escape internal quotes" , he said.
"""

def ss = '''This
That, The Other'''

def xml = """
<book id="987">
  <title>Groovy Recipes</title>
  <author>Scott Davis</author>
</book>"""

def html = """<body οnlοad="init()">...</body>"""

Heredocs [^312]支持多种动态语言,从Python到Perl到Ruby。 Heredoc允许您将多行字符串存储在单个变量中。 Groovy使用三引号(三个单引号或三个双引号)来定义Heredocs。

即使您的字符串是单行的,heredocs仍然非常有价值。 将XML,HTML或JSON片段放入变量中是进行单元测试的好方法。 不必转义内部引号可以轻松地将输出复制到变量中并立即开始针对该变量编写断言。

有关heredocs实际应用的示例,请参阅第239页第12.4节,设置Atom提要。

3.13 Groovy的String

def name = "John"
println "Hello ${name}. Today is ${new Date()}"
===> Hello John. Today is Fri Dec 28 15:16:32 MDT 2007

对于任何使用Ant构建文件或Java服务器页面(jsp)的人来说,字符串中嵌入的美元符号和大括号都是一个熟悉的场景。它使字符串连接比传统Java容易得多: "Hello " + name + "."。 Groovy以GString(当然是“Groovy字符串”的缩写)的形式将这种语法引入到语言中。任何包含表达式的字符串都是GString:

println "Hello John".class
===> class java.lang.String

println "Hello ${name}".class
===> class org.codehaus.groovy.runtime.GStringImpl

混合GString和heredocs(上一页的第3.12节,heredocs(三重引号))构成了一个特别强大的组合:

def name = "John"
def date = new Date()
def amount = 987.65
def template = """
Dear ${name},
  This is a friendly notice that ${amount} was
  deposited in your checking account on ${date}.
"""

3.14 列表快捷方式

def languages = ["Java", "Groovy", "JRuby"]
println languages.class
===> java.util.ArrayList

Groovy为创建ArrayLists提供了一种简洁的语法。

将以逗号分隔的值列表放在等号右边的方括号中,就得到了一个列表。(Maps也提供了类似的简单构造—参见第62页3.15节,Map快捷方式)。

虽然在默认情况下,方括号将提供一个ArrayList,但您可以在行尾加上as子句,以转换成其他各种数据类型。例如:

def languages = ["Java", "Groovy", "JRuby"] as String[]
def languages = ["Java", "Groovy", "JRuby"] as Set

创建一个空列表

def empty = []
println empty.size()
===> 0

要创建空列表,只需使用空集符号。

添加一个元素

def languages = ["Java", "Groovy", "JRuby"]
languages << "Jython"
===> [Java, Groovy, JRuby, Jython]

向列表中添加项很容易。Groovy将<<操作符重载到leftShift()方法,以完成此任务。(有关操作符重载的更多信息,请参见第50页第3.7节,操作符重载。)

得到一个元素

def languages = ["Java", "Groovy", "JRuby"]
println languages[1]
println languages.getAt(1)
==> Groovy

即使从技术上讲是列表,您也可以对其进行类似数组的调用。 Groovy模糊了列表和数组之间的语法区别,使您可以使用最喜欢的样式。

Iterating(迭代)

def languages = ["Java", "Groovy", "JRuby"]

//使用默认的“it”变量:
languages.each{println it}
===>
Java
Groovy
JRuby

//使用您选择的指定变量:
languages.each{ lang ->
  println lang
}
===>
Java
Groovy
JRuby

遍历列表是一种常见的活动,因此Groovy为您提供了一种方便的方法。在第一个示例中,使用迭代器变量的默认名称it。在第二个示例中,您显式地将变量命名为lang

当然,您仍然可以使用所有传统的Java方法来遍历列表。如果你喜欢Java 5 的 for..in语法或 java.util.Iterator迭代器,您可以继续使用它。请记住Groovy增强了Java;它不会取代它。

使用索引进行迭代

def languages = ["Java", "Groovy", "JRuby"]
languages.eachWithIndex{lang, i ->
  println "${i}: ${lang}"
}
===>
0: Java
1: Groovy
2: JRuby

"eachWithIndex()"为您提供当前元素和计数器变量。

Sort(排序)

def languages = ["Java", "Groovy", "JRuby"]
languages.sort()
===> [Groovy, JRuby, Java]
println languages
===> [Groovy, JRuby, Java]

您可以轻松地对列表进行排序。请注意,这是一个永久性的更改。sort()修改原始列表的内部排序顺序。

Reverse(反转)

def languages = ["Java", "Groovy", "JRuby"]
languages.reverse()
===> [JRuby, Groovy, Java]
println languages
===> [Java, Groovy, JRuby]

您可以轻松地反转列表。注意reverse()不会修改列表的原始排序顺序。它返回一个新列表。

Pop(弹出)

def languages = ["Java", "Groovy", "JRuby"]
languages.pop()
===> "JRuby"
println languages
===> [Java, Groovy]

您可以从列表中弹出内容。 pop方法使用LIFO,表示后进先出。 请注意,这是永久更改。 pop()从列表中删除最后一项。

Concatenating(级联)

def languages = ["Java", "Groovy", "JRuby"]
def others = ["Jython", "JavaScript"]
languages += others
===> [Java, Groovy, JRuby, Jython, JavaScript]
languages -= others
===> [Java, Groovy, JRuby]

您可以轻松地将两个列表添加在一起。你可以很容易地再减去它们。

Join(连接)

def languages = ["Java", "Groovy", "JRuby"]
groovy> languages.join()
===> JavaGroovyJRuby
groovy> languages.join(",")
===> Java,Groovy,JRuby

便捷方法join()返回一个字符串,其中包含List中的每个元素。 如果将字符串参数传递给join(),则每个元素将由字符串分隔。

Find All(找到所有)

def languages = ["Java", "Groovy", "JRuby"]
languages.findAll{ it.startsWith("G") }
===> [Groovy]

findAll()允许查询列表。它返回一个新列表,其中包含与您的条件匹配的所有元素。

Max, Min, Sum(最大,最小,总和)

def scores = [80, 90, 70]
println scores.max()
===> 90
println scores.min()
===> 70
println scores.sum()
===> 240

max()返回列表中的最大值。 min()返回最小值。 sum()汇总列表中的所有元素。

Collect(收集)

def languages = ["Java", "Groovy", "JRuby"]
languages.collect{ it += " is cool"}
===> [Java is cool, Groovy is cool, JRuby is cool]

如果要修改列表中的每个元素,则可以使用collect()方法。 请注意,collect()不会修改原始列表。 它返回一个新的列表。

Flatten(展平)

def languages = ["Java", "Groovy", "JRuby"]
def others = ["Jython", "JavaScript"]
languages << others
===> [Java, Groovy, JRuby, [Jython, JavaScript]]
languages = languages.flatten()
===> [Java, Groovy, JRuby, Jython, JavaScript]

如果您具有多维列表,则flatten()返回一维数组。 请注意,flatten()不会修改原始列表。 它返回一个新的列表。

Spread Operator (*) (点差算子)

def params = []
params << "jdbc:mysql://localhost:3306/bookstore_dev?autoreconnect=true"
params << "com.mysql.jdbc.Driver"
params << "username"
params << "password"
def sql = groovy.sql.Sql.newInstance(*params)

顾名思义,spread运算符将List的元素展开。 在此示例中,newInstance方法需要四个字符串参数。 *params接受List并将元素散布到方法参数的每个插槽中。

spread-dot(扩展点)运算符的工作方向相反。 它允许您简洁地遍历列表,在每个元素上调用相同的方法:

def languages = ["Java", "Groovy", "JRuby"]
println languages*.toUpperCase()
===> [JAVA, GROOVY, JRUBY]

3.15 映射快捷方式

def family = [dad:"John", mom:"Jane"]
println family.getClass()
===> java.util.LinkedHashMap

Groovy为创建映射提供了简洁的语法。您只需在等号右边的方括号中放入逗号限制的名称/值对列表,就得到了一个映射。列表提供了一个类似的简单构造—参见第3.14节,列表快捷方式,在第58页。

创建一个空映射

def empty = [:]
println empty.size()
===> 0

要创建空映射,只需使用带冒号的空集表示法。

得到一个元素

def family = [dad:"John", mom:"Jane"]
family.get("dad")
family.dad
===> John

您可以使用传统的Java get()方法从Map中返回一个元素。 但是,Groovy缩短了此语法,使其看起来就像您在直接调用该键一样。

如果您想使用类似数组的语法,那么family [‘dad’]是从Map中获取元素的另一种方法。

陷阱:.class为什么在除Map之外的所有类上都能工作?

def family = [dad:"John", mom:"Jane"]
println family.class
===> null
println family.getClass()
===> java.util.LinkedHashMap

由于使用点表示法将元素从Map中取出,因此调用map.class返回null而不是类类型。 为什么? 因为您的Map不包含名为class的元素。 对于Map,必须使用方法调用的长Java形式:map.getClass()。 当然,getClass()可在所有类中使用,因此,如果您希望100%的时间使用它,则这可能是最安全的调用形式。

需要更多信息,请参阅第73页上的侧栏。

添加元素

def family = [dad:"John", mom:"Jane"]
family.put("kid", "Timmy")
family.kid2 = "Susie"
===> {dad=John, mom=Jane, kid=Timmy, kid2=Susie}

您可以使用传统的Java put()方法将元素添加到Map中。 Groovy将其缩短为用于获取元素的相同的点号。

如果您希望使用类似数组的语法,family[’kid2’] = "Susie"也是有效的。

Iterating(迭代)

def family = [dad:"John", mom:"Jane"]

//using the default 'it' variable:
family.each{println it}
===>
dad=John
mom=Jane

//getting the key and value from 'it'
family.each{println "${it.value} is the ${it.key}" }
===>
John is the dad
Jane is the mom

//using named variables for the key and value
family.each{k,v ->
  println "${v} is the ${k}"
}
===>
John is the dad
Jane is the mom

遍历映射是一种常见的活动,因此Groovy为您提供了一种方便的方法。第一个示例使用迭代器变量的默认名称it。下一个示例将使用it.keyit.value获取名称/值对的单独部分。最后一个示例显式地分别命名键和值变量k和v

Concatenating(连接)

def family = [dad:"John", mom:"Jane"]
def kids = [kid:"Timmy", kid2:"Susie"]
family += kids
===> {dad=John, kid=Timmy, kid2=Susie, mom=Jane}

kids.each{k,v->
  family.remove("${k}")
}
===> {dad=John, mom=Jane}

您可以轻松地将两个Map添加在一起。 Groovy没有提供从另一个Map中减去一个Map的捷径,但是语法太短了,至多只是一个小小的疏忽。

Finding Keys(查找键)

def family = [dad:"John", mom:"Jane"]
family.keySet()
===> [dad, mom]
family.containsKey("dad")
===> true

您可以使用与Java的keySet()中相同的策略来查找映射的键,该策略返回所有键的列表,containsKey()让您知道键是否存在。

Finding Values(查找值)

def family = [dad:"John", mom:"Jane"]
family.values()
===> [John, Jane]
family.containsValue("John")
===> true

您可以使用与Java的values()中相同的策略来查找Groovy中的Map值,它返回所有值的列表,containsValue()让您知道某个值是否存在。

3.16 范围

def r = 1..3
println r.class
===> groovy.lang.IntRange
r.each{println it}
===>
1 2 3
r.each{ println "Hi" }
===>
Hi
Hi
Hi
(1..3).each{println "Bye"}
===>
Bye
Bye
Bye

Groovy为范围提供了一种本机数据类型。您可以在变量中存储一个范围,也可以动态地创建和使用它们。

为了简单起见,这里的所有示例都使用整数。但范围要灵活得多。它们可以包含实现Comparable接口并具有next()previous()方法的任何类。考虑一下这个日期范围的快速示例:

def today = new Date()
===> Sat Dec 29 23:59:28 MST 2007
def nextWeek = today + 7
===> Sat Jan 05 23:59:28 MST 2008
(today..nextWeek).each{println it}
===>
Sat Dec 29 23:59:28 MST 2007
Sun Dec 30 23:59:28 MST 2007
Mon Dec 31 23:59:28 MST 2007
Tue Jan 01 23:59:28 MST 2008
Wed Jan 02 23:59:28 MST 2008
Thu Jan 03 23:59:28 MST 2008
Fri Jan 04 23:59:28 MST 2008
Sat Jan 05 23:59:28 MST 2008

Size, From, To(大小,从,到)

def r = 1..3
r.size()
===> 3
r.from
===> 1
r.to
===> 3

我们可以询问范围的大小,起点和终点

For(for循环)

for(i in 1..3){ println "Attempt ${i}" }
===>
Attempt 1
Attempt 2
Attempt 3
(1..3).each{ println "Attempt ${it}" }
===>
Attempt 1
Attempt 2
Attempt 3

范围通常用于for循环,尽管直接在范围上调用each更为简洁。

Contains(包含)

def r = 1..3
r.contains(1) && r.contains(3)
===> true
r.contains(2)
===> true
r.contains(12)
===> false

范围可以告诉您任意值是否落在该范围内。 起点和终点都包括在范围内。

Reverse(反转)

r.reverse()
===> [3, 2, 1]

如果您需要向后遍历Range,则有一个方便的reverse()方法。

3.17 闭包和块

def hi = { println "Hi"}
hi()
===> Hi

groovy.lang.Closure最简单的形式是一个独立的,命名的代码块。 它是没有周围类的行为。

实际上,闭包并不是一个完全陌生的概念。我们在Java中有代码块(if、for、while、try、catch等),只是没有命名的代码块。Groovy增加了这种微小的语义差异,并在很大程度上利用了它。(有关闭包的实际应用示例,请参见219页11.8节“理解控制器和视图”。)

如果您不认为这是严格意义上的学术意义[^317],那么我很谦虚地表示歉意。 我还将有意识地避免使用诸如“ lambda样式的函数式编程”之类的短语[^318]。我并不是很讨厌-事情的简单事实是实现类名为Closure。

接受参数

def hello = { println "Hi ${it}" }
hello("John")
hello "John"
===> Hi John

熟悉的匿名it参数在第3.14节中讨论过,列表快捷方式在第58页,第3.15节中讨论过映射快捷方式在第62页。请注意,在调用闭包时可以省略括号,就像在调用方法时一样。(更多信息见第44页3.3节,可选括号。)

下面是一个更高级的闭包实例。注意如何在each和convertToCelsius闭包中使用it参数。

def convertToCelsius = {
  return (5.0/9.0) * (it.toFloat() - 32.0)
}
[0, 32, 70, 100].each{
  println "${it} degrees fahrenheit in celsius: ${convertToCelsius(it)}"
}

===>
0 degrees fahrenheit in celsius: -17.7777777792
32 degrees fahrenheit in celsius: 0.0
70 degrees fahrenheit in celsius: 21.1111111128
100 degrees fahrenheit in celsius: 37.7777777808

命名参数

def calculateTax = { taxRate, amount ->
  return amount + (taxRate * amount)
}
println "Total cost: ${calculateTax(0.055, 100)}"
===> Total cost: 105.500

尽管匿名it参数在编写快速的临时脚本时非常方便,但是从长远来看,命名参数将有助于提高代码的可读性和可维护性。如果您的闭包需要多个参数,那么除了为它们命名之外别无选择。

Currying Parameters(固化参数)

def calculateTax = { taxRate, amount ->
  return amount + (taxRate * amount)
}

def tax = calculateTax.curry(0.1)
[10,20,30].each{
  println "Total cost: ${tax(it)}"
}
===>
Total cost: 11.0
Total cost: 22.0
Total cost: 33.0

在实例化闭包时,可以使用curry方法将值预加载到参数中。在本例中,为taxRate硬编码一个默认值将显著降低闭包的可重用性。另一方面,每次调用闭包时都必须传递相同的税率,这是不必要的重复和冗长。提高税率正好达到了恰当的平衡。

您可以根据需要使用任意数量的参数。 第一个curry调用将填充最左侧的参数。 每个后续调用将填充右侧的下一个参数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱游泳的老白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值