Groovy 中的类
迄今为止,您已经用 Groovy 输出了许多次 “Hello World”,已经操作了集合,用闭包在集合上迭代,也定义了您自己的闭包。做所有这些工作时,甚至还没有讨论那个对 Java 开发人员来说至关重要的概念 — 类。
当然,您已经在这个教程中使用过类了:您编写的最后几个示例就是在不同类的 main() 方法中。而且,您已经知道,在 Groovy 中可以像在 Java 代码中一样定义类。惟一的区别是,不需要使用 public 修改符,而且还可以省略方法参数的类型。这一节将介绍使用 Groovy 类能够进行的其他所有操作。
Song 类
我们先从用 Groovy 定义一个简单的 JavaBean 形式的类开始,这个类称为 Song。
第一步自然是用 Groovy 创建名为 Song 的类。这次还要为它创建一个包结构 — 创建一个包名,例如 org.acme.groovy。
创建这个类之后,删除 Groovy 插件自动生成的 main()。
歌曲有一些属性 — 创作歌曲的艺术家、歌曲名称、风格等等。请将这些属性加入新建的 Song 类,如下所示:
package org.acme.groovy
class Song {
def name
def artist
def genre
}
迄今为止还不错,是不是?对于 Grooovy 的新开发人员来说,还不算太复杂!
Groovy 类就是 Java 类
应该还记得本教程前面说过 Groovy 编译器为用 Groovy 定义的每个类都生成标准的 Java .class。还记得如何用 Groovy 创建 HelloWorld 类、找到 .class 文件并运行它么?也可以用新定义的 Song 类完成同样的操作。如果通过 Groovy 的 groovyc 编译器编译代码(Eclipse Groovy 插件已经这样做了),就会生成一个 Song.class 文件。
这意味着,如果想在另一个 Groovy 类或 Java 类中使用新建的 Song 类,则必须导入 它(当然,除非使用 Song 的代码与 Song 在同一个包内)。
接下来创建一个新类,名为 SongExample,将其放在另一个包结构内,假设是 org.thirdparty.lib。
现在应该看到如下所示的代码:
package org.thirdparty.lib
class SongExample {
static void main(args) {}
}
类的关系
现在是使用 Song 类的时候了。首先导入实例,并将下面的代码添加到 SongExample 的 main() 方法中。
package org.thirdparty.lib
import org.acme.groovy.Song
class SongExample {
static void main(args) {
def sng = new Song(name:"Le Freak",
artist:"Chic", genre:"Disco")
}
}
现在 Song 实例创建完成了!但是仔细看看以前定义的 Song 类的初始化代码,是否注意到什么特殊之处?您应该注意到自动生成了构造函数。
类初始化
Groovy 自动提供一个构造函数,构造函数接受一个名称-值对的映射,这些名称-值对与类的属性相对应。这是 Groovy 的一项开箱即用的功能 — 用于类中定义的任何属性,Groovy 允许将存储了大量值的映射传给构造函数。映射的这种用法很有意义,例如,您不用初始化对象的每个属性。
也可以添加下面这样的代码:
def sng2 = new Song(name:"Kung Fu Fighting", genre:"Disco")
也可以像下面这样直接操纵类的属性:
def sng3 = new Song()
sng3.name = "Funkytown"
sng3.artist = "Lipps Inc."
sng3.setGenre("Disco")
assert sng3.getArtist() == "Lipps Inc."
从这个代码中明显可以看出,Groovy 不仅创建了一个构造函数,允许传入属性及其值的映射,还可以通过 . 语法间接地访问属性。而且,Groovy 还生成了标准的 setter 和 getter 方法。
在进行属性操纵时,非常有 Groovy 特色的是:总是会调用 setter 和 getter 方法 — 即使直接通过 . 语法访问属性也是如此
核心的灵活性
Groovy 是一种本质上就很灵活的语言。例如,看看从前面的代码中将 setGenre() 方法调用的括号删除之后会怎么样,如下所示:
sng3.setGenre "Disco"
assert sng3.genre == "Disco"
在 Groovy 中,对于接受参数的方法,可以省略括号 — 在某些方面,这样做会让代码更容易阅读。
方法覆盖
迄今为止已经成功地创建了 Song 类的一些实例。但是,它们还没有做什么有趣的事情。可以用以下命令输出一个实例:
println sng3
在 Java 中这样只会输出所有对象的默认 toString() 实现,也就是类名和它的 hashcode(即 org.acme.groovy.Song@44f787)。下面来看看如何覆盖默认的 toString() 实现,让输出效果更好。
在 Song 类中,添加以下代码:
String toString(){
"${name}, ${artist}, ${genre}"
}
根据本教程已经学到的内容,可以省略 toString() 方法上的 public 修改符。仍然需要指定返回类型(String),以便实际地覆盖正确的方法。方法体的定义很简洁 — 但 return 语句在哪?
不需要 return
您可能已经想到:在 Groovy 中可以省略 return 语句。Groovy 默认返回方法的最后一行。所以在这个示例中,返回包含类属性的 String。
重新运行 SongExample 类,应该会看到更有趣的内容。toString() 方法返回一个描述,而不是 hashcode。
特殊访问
Groovy 的自动生成功能对于一些功能来说很方便,但有些时候需要覆盖默认的行为。例如,假设需要覆盖 Song 类中 getGenre() 方法,让返回的 String 全部为大写形式。
提供这个新行为很容易,只要定义 getGenre() 方法即可。可以让方法的声明返回 String,也可以完全省略它(如果愿意)。下面的操作可能是最简单的:
def getGenre(){
genre.toUpperCase()
}
同以前一样,这个简单方法省略了返回类型和 return 语句。现在再次运行 SongExample 类。应该会看到一些意外的事情 —— 出现了空指针异常。
空指针安全性
如果您一直在跟随本教程,那么应该已经在 SongExample 类中加入了下面的代码:
assert sng3.genre == "Disco"
结果在重新运行 SongExample 时出现了断言错误 — 这正是为什么在 Eclipse 控制台上输出了丑陋的红色文字。(很抱歉使用了这么一个糟糕的技巧)
幸运的是,可以轻松地修复这个错误:只要在 SongExample 类中添加以下代码:
println sng2.artist.toUpperCase()
但是现在控制台上出现了更多的 红色文本 — 出什么事了?!
可恶的 null
如果回忆一下,就会想起 sng2 实例没有定义 artist 值。所以,在调用 toUpperCase() 方法时就会生成 Nullpointer 异常。
幸运的是, Groovy 通过 ? 操作符提供了一个安全网 — 在方法调用前面添加一个 ? 就相当于在调用前面放了一个条件,可以防止在 null 对象上调用方法。
例如,将 sng2.artist.toUpperCase() 行替换成 sng2.artist?.toUpperCase()。请注意,也可以省略后面的括号。(Groovy 实际上也允许在不带参数的方法上省略括号。不过,如果 Groovy 认为您要访问类的属性而不是方法,那么这样做可能会造成问题。)
重新运行 SongExample 类,您会发现 ? 操作符很有用。在这个示例中,没有出现可恶的异常。现在将下面的代码放在这个类内,再次运行代码。
def sng4 = new Song(name:"Thriller", artist:"Michael Jackson")
println sng4
就是 Java
您将会注意到,虽然预期可能有异常,但是没有生成异常。即使没有定义 genre,getGenre() 方法也会调用 toUpperCase()。
您还记得 Groovy 就是 Java,对吧?所以在 Song 的 toString() 中,引用了 genre 属性本身,所以不会调用 getGenre()。现在更改 toString() 方法以使用 getGenre(),然后再看看程序运行的结果。
String toString(){
"${name}, ${artist}, ${getGenre()}"
}
重新运行 SongExample,出现类似的异常。现在,请自己尝试修复这个问题,看看会发生什么。
另一个方便的小操作符
希望您做的修改与我的类似。在下面将会看到,我进一步扩充了 Song 类的 getGenre() 方法,以利用 Groovy 中方便的 ? 操作符。
def getGenre(){
genre?.toUpperCase()
}
? 操作符时刻都非常有用,可以极大地减少条件语句。
对 Groovy 进行单元测试
本教程一直都强调 Groovy 只是 Java 的一个变体。您已经看到可以用 Groovy 编写并使用标准的 Java 程序。为了最后一次证明这点,在结束本教程之前,我们将通过 JUnit 利用 Java 对 Song 类进行单元测试。
将 JUnit 加入 Eclipse 项目
为了跟上本节的示例,需要将 JUnit 加入到 Eclipse 项目中。首先,右键单击项目,选择 Build Path,然后选择 Add Libraries,如图 14 所示:
图 14. 将 JUnit 加入到项目的构建路径
会出现 Add Library 对话框,如图 15 所示。
图 15. 从库列表中选择 JUnit
选择 JUnit 并单击 Next 按钮。应该会看到如图 16 所示的对话框。选择 JUnit3 或 4— 具体选择哪项全凭自己决定 — 并单击 Finish 按钮。
图 16. 选择 JUnit 3 或 JUnit 4
设置新的测试用例
现在在项目的类路径中加入了 JUnit,所以能够编写 JUnit 测试了。请右键单击 java 源文件夹,选择 New,然后选择 JUnit Test Case。定义一个包,给测试用例命名(例如 SongTest),在 Class Under Test 部分,单击 Browse 按钮。
请注意,可以选择用 Groovy 定义的 Song 类。图 17 演示了这一步骤:
图 17.找到 Song 类
选择该类并单击 OK(应该会看到与图 18 类似的对话框)并在 New JUnit Test Case 对话框中单击 Finish 按钮。
图 18. Song 的新测试用例
定义测试方法
我选择使用 JUnit 4;所以我定义了一个名为 testToString() 的测试方法,如下所示:
package org.acme.groovy;
import org.junit.Test;
public class SongTest {
@Test
public void testToString(){}
}
测试 toString
显然,需要验证 toString() 方法是否没有问题,那么第一步该做什么呢?如果想的是 “导入 Song 类”,那么想得就太难了 —Song 类在同一个包内,所以第一步是创建它的实例。
在创建用于测试的 Song 实例时,请注意不能通过传给构造函数的映射完全初始化 — 而且,如果想自动完成实例的 setter 方法,可以看到每个 setter 接受的是 Object 而不是 String(如图 19 所示)。为什么会这样呢?
图 19. 所有的 setter 和 getter
Groovy 的功劳
如果回忆一下,就会记得我在本教程开始的时候说过:
因为 Java 中的每个对象都扩展自 java.lang.Object,所以即使在最坏情况下,Groovy 不能确定变量的类型,Groovy 也能将变量的类型设为 Object然后问题就会迎刃而解。 现在回想一下,在定义 Song 类时,省略了每个属性的类型。Groovy 将自然地将每个属性的类型设为 Object。所以,在标准 Java 代码中使用 Song 类时,看到的 getter 和 setter 的参数类型和返回类型全都是 Object。
修正返回类型
为了增添乐趣,请打开 Groovy Song 类,将 artist 属性改为 String 类型,而不是无类型,如下所示:
package org.acme.groovy
class Song {
def name
String artist
def genre
String toString(){
"${name}, ${artist}, ${getGenre()}"
}
def getGenre(){
genre?.toUpperCase()
}
}
现在,回到 JUnit 测试,在 Song 实例上使用自动完成功能 — 看到了什么?
在图 20 中(以及您自己的代码中,如果一直跟随本教程的话),setArtist() 方法接受一个 String,而不是Object。Groovy 再次证明了它就是 Java,而且应用了相同的规则。
图 20. String,而不是 object
始终是普通的 Java
返回来编写测试,另外请注意,默认情况下 Groovy 编译的类属性是私有的,所以不能直接在 Java 中访问它们,必须像下面这样使用 setter:
@Test
public void testToString(){
Song sng = new Song();
sng.setArtist("Village People");
sng.setName("Y.M.C.A");
sng.setGenre("Disco");
Assert.assertEquals("Y.M.C.A, Village People, DISCO",
sng.toString());
}
编写这个测试用例余下的代码就是小菜一碟了。测试用例很好地演示了这样一点:用 Groovy 所做的一切都可以轻易地在 Java 程序中重用,反之亦然。用 Java 语言执行的一切操作和编写的一切代码,在 Groovy 中也都可以使用。
结束语
如果说您从本教程获得了一个收获的话(除了初次体验 Groovy 编程之外),那么这个收获应该是深入地认识到 Groovy 就是 Java,只是缺少了您过去使用的许多语法规则。Groovy 是没有类型、没有修改符、没有 return、没有 Iterator、不需要导入集合的 Java。简而言之,Groovy 就是丢掉了许多包袱的 Java,这些包袱可能会压垮 Java 项目。
但是在幕后,Groovy 就是 Java。
我希望通向精通 Groovy 的这第一段旅程给您带来了快乐。您学习了 Groovy 语法,创建了几个能够体验到 Groovy 的生产力增强功能的类,看到了用 Java 测试 Groovy 类有多容易。还遇到了第一次使用 Groovy 的开发者常见的一些问题,看到了如何在不引起太多麻烦的情况下解决它们。
尽管您可能觉得自己目前对 Groovy 还不是很熟练,但您已经走出了第一步。您可以用目前学到的知识编写自己的第一个 Groovy 程序 — 毕竟,您已经设置好了同时支持 Groovy 和 Java 编程的双重环境!作为有趣的练习,您可以试试用 Gant 设置下一个的自动构建版本,Gant 是基于 Ant 的构建工具,使用 Groovy 来定义构建,而不是使用 XML。当您对 Groovy 更加适应时,可以试着用 Groovy on Grails 构建 Web 应用程序模块 — 顺便说一下,这是下一篇教程的主题。