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

《Java程序员修炼之道》8.2 Groovy 101:语法和语义

关灯直达底部

上一节只写了一行Groovy语句,没有任何类或方法之类的结构(用Java时会需要)。实际上你写的是一个Groovy脚本。

Groovy脚本

跟Java不同,Groovy的源码可以当做脚本执行。比如说,如果你有一段代码放在类定义之外,那段代码还是可以执行。像其他动态脚本语言(比如Ruby或Python)一样,Groovy脚本在JVM上执行之前要在内存中经过完整的分析、编译和生成过程。任何能在Groovy控制台执行的代码都可以保存到.groovy文件,经过编译后,就可以作为脚本运行。

一些开发人员已经用Groovy脚本取代了shell脚本,因为它们功能更强,更易于编写,并且只要装了JVM,就可以在任何平台上运行。给你一个性能方面的小提示,请使用groovyserv类库,它会启动JVM和Groovy扩展,让脚本运行得更快。

Groovy的一个关键特性是可以使用跟Java中一样的结构,语法也类似。为了突出这种相似性,请在Groovy控制台中执行下面这段类似Java的代码:

public class PrintStatement{  public static void main(String args)  {    System.out.println(/"It/'s Groovy baby, yeah!/");  }}  

结果和前面那个只有一行的Groovy脚本一样,都是输出/"It/'s Groovy baby, yeah!/"。除了使用Groovy控制台,你还可以把源码放到PrintStatement.groovy源文件中,用groovyc编译它,然后用groovy执行。换句话说,你能像Java中那样带着类和方法编写Groovy源码。

提示 在Groovy中几乎可以使用所有Java普通语法,所以while/for循环、if/else结构、switch语句等,都会按你期望的方式工作。所有新语法及主要差异都会在本节及相应章节中重点阐述。

随着本章内容的深入,我们会向你介绍Groovy特有的语法惯用语,例子也会从类似Java的语法向更纯粹的Groovy语法转变。你已经习惯了结构沉重的Java代码,再见到像脚本一样简洁的Groovy语法,很容易发现两者的差异。

本节的剩余部分会介绍Groovy的基本语法和语义,以及它们为什么能帮助开发人员。具体来说,我们会探讨:

  • 默认导入;
  • 数字处理;
  • 变量、动态与静态类型,以及作用域;
  • 列表与映射的语法。

首先,理解Groovy提供了哪些开箱即用的东西很重要。我们先来看看Groovy脚本或程序的默认导入。

8.2.1 默认导入

Groovy会默认导入一些语言包和工具包,以提供基本的语言支持。Groovy还会导入一系列的Java包,以便为其初始功能提供更广泛的基础。下面这个导入列表总是隐含在Groovy代码之中:

  • groovy.lang.*
  • groovy.util.*
  • java.lang.*
  • java.io.*
  • java.math.BigDecimal
  • java.math.BigInteger
  • java.net.*
  • java.util.*

要使用更多的包和类,可以像Java一样用import语句。比如要从Java中得到所有Math类,只要在Groovy源码里加上import java.math.*;就行了。

设置可选的JAR文件

为了添加功能(比如内存数据库及其驱动),可以在Groovy安装中添加可选JAR。Groovy为此提供了一个惯用语:通常是在脚本中使用@Grab注解。另外一种办法(在你仍在学习Groovy时)是效仿Java,把JAR文件加到CLASSPATH中。

下面就来使用一下默认的语言支持,并看看Java和Groovy在数字处理上的差异。

8.2.2 数字处理

Groovy能动态计算数学表达式,并且它采用最小意外原则。这一原则在处理浮点数时(比如3.2)尤其明显。Groovy在底层用Java中的BigDecimal表示浮点数,但它会确保BigDecimal的行为尽量符合开发人员的期望。

1. Java和BigDecimal

我们来看一个经常会让开发人员头疼的数字处理问题。在Java中,如果在BigDecimal 3上加0.2,你觉得答案应该是什么?缺乏经验的Java开发人员在没看Javadoc的情况下很可能会执行下面这种代码,它会返回一个极其恐怖的结果:3.200000000000000011102230246251565404236316680908203125。

BigDecimal x = new BigDecimal(3);BigDecimal y = new BigDecimal(0.2);System.out.println(x.add(y));  

经验丰富的Java开发人员知道最好用BigDecimal(String val),而不是用将数字作为参数的BigDecimal 构造方法。以字符串为参数的构造方法写出来的代码会产生预期答案3.2:

BigDecimal x = new BigDecimal(/"3/");BigDecimal y = new BigDecimal(/"0.2/");System.out.println(x.add(y));  

这有点悖于常理,所以Groovy默认采用了以字符串为参数的构造方法,解决了这一问题。

2. Groovy和BigDecimal

在Groovy中处理浮点数(在底层用BigDecimal表示)时,会自动使用以字符串为参数的构造方法,3 + 0.2会得到3.2。你可以在Groovy控制台中输入下面的指令亲自证实一下:

3 + 0.2;  

你会发现Groovy对BEDMAS1的支持是正确的。并且在需要时能无缝切换数字类型(比如intdouble)。

1 回想起你在学校的日子了吧!BEDMAS表示括号、次方、除法、乘法、加法和减法,是我们计算数学题目时所要遵循的顺序(先计算括号和次方,再计算乘除,最后计算加减)。由于地区不同,你的记忆中可能是BODMAS或PEMDAS。

用Groovy进行数学运算比Java简单。如果你想了解底层细节,可以访问http://groovy.codehaus.org/Groovy+Math,那里有所有的细节信息。

接下来我们学习Groovy如何处理变量和作用域。因为Groovy的动态性和执行脚本的能力,它在这方面的语义规则和Java稍有不同。

8.2.3 变量、动态与静态类型、作用域

因为Groovy是一种能作为脚本语言的动态语言,所以你要清楚动态类型和静态类型一些细微差别,还需要了解Groovy如何限定变量的作用域。

提示 如果你意在让Groovy代码与Java互操作,它也能在可能的情况下使用静态类型,因为它简化了类型重载和调度机制。

首先你要理解Groovy动态类型和静态类型的差别。

1. 动态类型与静态类型

Groovy是动态语言,所以不必指定变量的类型,变量的类型是在声明(或返回)时确定的。比如说,你可以把一个Date赋值给变量x,然后紧接着再用不同的类型给x赋值。

x = new Date;x = 1;  

用动态类型能让代码更简洁(忽略显而易见的类型信息),反馈更快,并且很灵活,可以在一个变量上赋予不同类型的对象来完成工作。对于那些想对自己使用的类型更有把握的人,Groovy也确实支持静态类型。比如:

Date x = new Date;  

如果声明了静态类型变量,在用不正确的类型值对它赋值时,Groovy能检查出来。比如:

Exception thrownorg.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast     object /'Thu Oct 13 12:58:28 BST 2011/' with class /'java.util.Date/' to     class /'double/'...  

在Groovy控制台中运行下面的代码,就可以重现上面的输出。

double y = -3.1499392;y = new Date;  

如你所料,Data类型的值不能赋给double变量。Groovy中的动态和静态类型都讨论到了,那作用域呢?

2. Groovy中的作用域

对于Groovy里的类,其作用域跟Java一样,类、方法、循环作用域的变量,它们的作用域都跟你想的一样。但涉及Groovy脚本时,这个话题就变得比较有意思了。

提示 记住,作为脚本的Groovy代码不在平常的类和方法结构中。8.1.1节已经给过一个例子了。

简单说,Groovy脚本有两种作用域。

  • 绑定域,绑定域是脚本的全局作用域。

  • 本地域,本地域就是变量的作用域局限于声明它们的代码块。对于在脚本代码块内声明的变量(比如在脚本的顶部),如果是定义过的变量,其作用域就是定义它的本地域。

能在脚本中使用全局变量可以极大提高代码的灵活性。它和Java中类范围内的变量有点像。定义变量是指被声明为静态类型,或用特殊的def关键字定义的变量(表明它是未确定类型的定义变量)。

在脚本中声明的方法访问不了本地域。如果你调用一个试图引用本地域中的变量的方法,会提示类似下面的错误消息:

groovy.lang.MissingPropertyException: No such property: hello for class:     listing_8_2...  

下面是产生该异常的代码,说明了作用域的这个问题。

String hello = /"Hello!/";void checkHello{  System.out.println(hello);}checkHello;  

如果用hello = /"Hello!/"换掉上面代码里的第一行,这个方法可以成功输出“Hello”。因为hello不再定义为String,它现在的作用域是 绑定域。

除了编写Groovy脚本时的这些差异,动态和静态类型、作用域、变量声明都跟你想的完全一样。接下来我们去看看Groovy内置的集合(列表和映射)支持。

8.2.4 列表和映射语法

Groovy把列表和映射(包括集合)结构当做语言中的一等公民对待,所以没必要像Java那样显式声明ListMap结构。也就是说,Groovy中的列表和映射在底层是由Java ArrayListLinkedHashMap实现的。

使用Groovy语法最大的优势在于可以省掉很多套路化的代码,让代码更简洁,但丝毫不影响可读性。

Groovy用方括号指定和使用列表结构(是不是想起了Java中的原生数组语法)。下面的代码展示了如何引用第一个元素(Java),获取列表大小(4),以及将列表设置为空

jvmLanguages = [/"Java/", /"Groovy/", /"Scala/", /"Clojure/"];println(jvmLanguages[0]);println(jvmLanguages.size);jvmLanguages = ;println(jvmLanguages);  

看,Groovy将列表作为一等公民处理要比用java.util.List及其实现类的代码轻量得多。

因为Groovy是动态类型语言,我们可以把不同类型的值保存在列表(或映射)中,所以下面的代码也是正确的:

jvmLanguages = [/"Java/", 2, /"Scala/", new Date];  

Groovy处理映射也跟这差不多,用符号,并用冒号(:)来分开键/值对。以映射.键的方式引用映射中的值。下面的代码通过相应的操作展示了这些功能:

  • 引用键/"Java/"的值100
  • 引用键/"Clojure/"的值/"N/A/"
  • 将键/"Clojure/"的值变成75
  • 将映射设为空([:])。

    languageRatings = [Java:100, Groovy:99, Clojure:/"N/A/"];println(languageRatings[/"Java/"]);println(languageRatings.Clojure);languageRatings[/"Clojure/"] = 75;println(languageRatings[/"Clojure/"]);languageRatings = [:];println languageRatings;  

提示 你有没有注意到映射里的键是不带引号的字符串?为了让代码更简洁,Groovy对这个语法也做了调整,映射键的引号可用可不用。

这种写法很直观,用起来也舒服。Groovy把对映射和列表内置支持的概念更进了一步。

还有一些语法技巧,比如引用集合中一定范围内的元素,甚至可以用负索引引用最后一个元素。下面的代码引用了列表中的前三个元素([Java, Groovy, Scala])和最后一个元素(Clojure)。

jvmLanguages = [/"Java/", /"Groovy/", /"Scala/", /"Clojure/"];println(jvmLanguages[0..2]);println(jvmLanguages[-1]);  

现在,我们已经了解了Groovy的一些基本语法和语义。但在真正使用Groovy之前,还需要学习更多内容。下一节会更深入地探讨Groovy的语法和语义,重点讲解Java开发人员学习Groovy过程中那些“难缠的内容”。