11.2 Groovy语言

在很大程度上说,Groovy语言与Java语言兼容,因此对Java开发人员来说,Groovy学起来很容易。在Java中很多必不可少的元素在Groovy中都是可选的。Groovy使用的语义与Java相同,因此本章重点介绍Java和Groovy的不同之处。

Groovy的根本宗旨是紧凑、舒心和灵活。下面先来看一个简单的Java类:

  1. class Person {
  2. private String name;
  3. public String getName() {
  4. return name;
  5. }
  6. public void setName(String name) {
  7. this.name = name;
  8. }
  9. public static void main(String[] args) {
  10. Person p = new Person();
  11. p.setName("fooBar");
  12. System.out.println(p.getName());
  13. }
  14. }

这个类在Groovy中能够编译并运行,你可以在GroovyConsole中输入并执行这些代码。然而,通过利用Groovy特有的结构,可使用少得多的代码来编写这个程序:

  1. class Person {
  2. String name
  3. static void main(String[] args) {
  4. def p = new Person()
  5. p.name = "fooBar"
  6. println p.name
  7. }
  8. }

下面来详细说说这些代码。

  • Groovy不要求代码行以分号结尾。
  • 只需通过指定属性的类型和名称就可创建它(稍后你将看到,属性的类型也是可选的)。Groovy将自动创建一个私有变量以及公有的获取函数和设置函数,在这里它们分别是namegetName()setName()
  • 在Groovy中,访问限定符默认为public,因此static void main()与Java代码public static void main()等效。
  • 使用def来声明变量时,无需在赋值语句的左侧指定类型。
  • println是Groovy开发包中的一个内置函数,相比于System.out.println,它更紧凑。
  • Groovy甚至不要求你使用括号(())将方法名和传入的值分开。只要编译器确定不存在二义性,就会允许你这样做。在前述代码中,println p.name完全合法,编译器不会提出异议。
  • 可直接通过属性名来访问属性。在前面的示例中,Groovy将分别调用方法setName()getName()

在本书后面,将更详细地介绍前述众多要点。

虽然在Groovy中括号并非必不可少,但这并不意味着在任何情况下省略它们都是好主意。很多程序员发现,通过用括号将方法名和输入参数分开,可提高代码的可读性。

11.2 Groovy语言 - 图1 Groovy官方风格指南推荐在特定的情况下省略括号。本书不介绍Groovy风格指南,但你可参阅http://groovy-lang.org/style-guide.html

Groovy面向对象编程

在面向对象编程方面,下面这些无疑是Java和Groovy最大的不同之处。

  • 不同于Java,Groovy是一种完全面向对象编程的语言。
  • 在Groovy中,默认访问限定符为public
  • Groovy能够自动为属性创建获取和设置函数。
  • 对于属性、变量以及方法的参数和返回值,可显式地指定它们的类型,也可不指定。
  • Groovy能够自动创建功能齐备的POJO。
  • 可自动生成不可变类。

  • Groovy是完全面向对象的

相比于Java,Groovy的一个不同之处在于它是完全面向对象的:它不创建基本类型值,而是在任何情况下都创建对象。为验证这一点,请在GroovyConsole(或GroovyShell)中运行下面的脚本:

  1. int i = 555
  2. i.getClass()

这将打印java.lang.Integer(这可能让你感到意外)。在Java中,这将创建一个int值,进而拒绝编译上述代码,但Groovy在任何情况下都创建对象。然而,Groovy完全与Java工具和库兼容,因为它将基本类型值自动装箱为包装类,反之亦然。

  • 访问限定符

在程序员没有显式给方法或类/实例变量指定访问限定符时,Java认为它是包私有的,而Groovy默认使用访问限定符public

11.2 Groovy语言 - 图2 这是一个重要的差别。有鉴于此,大部分Java代码虽然在语法层面与Groovy兼容,但最终的行为可能不同,并可能以无法预料的方式修改程序。

Groovy支持如下访问限定符:

  • public
  • protected
  • private
    在没有显式地指定访问限定符时,Groovy默认使用public;同时Groovy没有提供与Java包私有对应的关键字。有鉴于此,Groovy本身不支持创建包私有的类或成员。

11.2 Groovy语言 - 图3 为应对必须使用包私有成员的情形(这种情形很少见),Groovy开发包(GDK)提供了注解@PackageScope,你可从groovy.transform包中导入它。然而,本书不会介绍这个高级主题。

必须指出的是,在类的私有成员方面,Groovy存在一个由来已久的bug:不遵守有关访问限定符private的约定。下面的Java代码无法通过编译(在任何遵守有关访问限定符private的约定的语言中,类似的代码都无法通过编译),但在当前的Groovy版本中却能畅通无阻地运行:

  1. public class MainDemoClass {
  2. public static void main(String[] args) {
  3. ClassWithSecret secret = new ClassWithSecret();
  4. System.out.println(secret.privateVariable);
  5. }
  6. }
  7. class ClassWithSecret {
  8. private int privateVariable = -1;
  9. }

实例变量privateVariable使用了访问限定符private,在其所属的类ClassWithSecret外面应该是不可见的。在生成的Java字节码中,Groovy编译器正确地将privateVariable设置成了private的,因此在Java等语言中使用这个类时,将无法在类似于上述的代码中访问 privateVariable(除非采用访问私有数据的低级技巧)。

11.2 Groovy语言 - 图4 在这方面,Groovy的行为类似于Python等动态语言。Python其实并没有公有和私有成员的概念——一切都是公有的。多年来,对于Groovy不支持限定符private是bug还是特色一直争论不休。

  • 给类添加属性

对于没有显式地指定访问限定符(publicprivateprotected)的属性,Groovy编译器将自动为它创建一个同名的私有变量,并生成公有的设置函数和获取函数:

  1. class Person {
  2. String name
  3. }

上述代码创建的类如下:

11.2 Groovy语言 - 图5

Groovy编译这些代码时,将完成如下工作。

  • 创建私有变量name
  • 创建类似于public void String getName()的公有获取函数。
  • 创建类似于public void setName(String name)的公有设置函数。
    仅当没有指定访问限定符时,编译器才会这样做。只要指定了访问限定符(无论是privatepublic还是protected),编译器就认为开发人员想全面控制属性,进而只创建一个变量,让开发人员根据需要决定是否创建获取函数和/或设置函数。

与Kotlin一样,Groovy也不要求通过调用获取函数和设置函数来访问属性,只需使用属性名即可。请在前面的示例中添加如下代码,再运行它:

  1. p = new Person()
  2. p.name = "D. Vader"
  3. println(p.name)

这些代码调用Groovy自动创建的方法getName()setName()。为证明这些代码没有利用前面提及的访问限定符private存在的bug,请将Person类的代码替换为如下代码:

  1. class Person {
  2. private String personName;
  3. public void setName(String name) { this.personName = name }
  4. public String getName() { return this.personName }
  5. }

如果你尝试运行这些代码,将发现它们能够畅通无阻地运行而且管用,虽然现在没有私有变量name。Groovy确实调用了setName()getName()

  • 类型是可选的

Groovy全面支持Java的声明风格。在Java中,同时指定变量类型和使用的实例类型,如下所示:

  1. Date date = new Date();

在Groovy中,声明变量时可不指定其类型,为此可使用关键字def

  1. def date = new Date()

这类似于Java语句Object date = new Date();差别在于在Groovy中使用关键字def声明变量date时,通过这个变量可访问java.util.Date类的所有成员:

  1. def date = new Date()
  2. println date.getTime()

在Java中,必须将Object实例向下转换为java.util.Date实例,才能访问java.util.Date的成员:

  1. // Java代码(假定导入了java.util.Date)
  2. Object date = new Date();
  3. System.out.println(((java.util.Date)date).getTime());

如你所见,Groovy代码不但简洁得多,可读性也更强。在这方面,Java和Groovy的主要差别在于,Java会在编译阶段检查方法是否可用。Java编译器检查在变量的引用类型中是否有指定的方法。因此,只有将这个变量向下转换为java.util.Date类后,它才会接受方法getTime(),因为java.lang.Object类没有方法getTime()。而Groovy更灵活,它等到运行阶段才尝试调用方法,并在对象不支持调用的方法和/或传入的参数时引发异常。Groovy不关心引用变量的类型。

需要指出的是,使用def声明的变量可指向任何类型的对象:

  1. def d = new Date()
  2. d = new ArrayList()

对于方法的参数和返回值,也可不指定类型:

  1. def methodWithParameters(parm1, parm2, parm3) {
  2. // 代码……
  3. }

对于这里的参数parm1parm2parm3以及返回值,类型为java.lang.Object

另外,return语句也是可选的。函数的返回值默认为其最后一个表达式:

  1. int methodWithImplicitReturnValue(int i) {
  2. i * 10
  3. }

最后,定义属性时,也可不显式地指定其类型:

  1. class Sensor {
  2. def temperature
  3. }

在这个示例中,将创建类型为java.lang.Object的私有变量temperature,还将创建获取函数public Object getTemperature()以及设置函数public void setTemperature (Object temperature)

11.2 Groovy语言 - 图6 在大多数情况下,最好还是指定类型,否则小组的其他开发人员必须阅读注释来确定哪些类型与你创建的方法和属性兼容。可你在任何情况下都会给代码添加注释吗?

  • 自动创建功能齐备的POJO

如果在类名前指定了注解@Canonical,Groovy将生成如下元素——如果类本身没有这些元素的自定义实现的话。

  • 不接受任何参数的构造函数。
  • 将每个属性都作为参数的构造函数(这些参数的排列顺序与属性的定义顺序相同)。
  • 方法toString()的实现,它打印所有的属性(名称和值)。
  • 方法hashCode()的实现。
  • 方法equals()的实现。
    如果类实现了toString()hashCode()equals(),Groovy将不考虑它们,继续检查其他元素是否实现了。因此,如果你在自定义类中实现了toString(),Groovy将使用该实现:
  1. import groovy.transform.Canonical
  2. @Canonical
  3. class CanonicalDemo {
  4. def property1
  5. def property2
  6. def property3
  7. }
  8. def demo = new CanonicalDemo("value for property1", "value for
  9. property2")
  10. println("${demo.property1}, ${demo.property2}, ${demo.property3}")
  11. println(demo)

11.2 Groovy语言 - 图7 这里演示了Groovy字符串的一种强大功能:它们提供了内置的模板支持,这将在后面更详细地讨论。

下图概述了为CanonicalDemo类生成的所有属性和方法。

11.2 Groovy语言 - 图8

前面的代码将打印value for property1, value for property2, nullCanonicalDemo(value for property1, value for property2, null)。注意,为Canoni calDemo类生成的方法toString()只按声明顺序打印属性的值,而没有打印属性的名称。

正如刚才演示的,并非必须给所有的属性都提供值。另外,也可只给特定的属性指定值:

  1. def demo = new CanonicalDemo(property1:"value 1", property3: "value3")

如果你只想让Groovy生成方法hashCode()equals()的实现,而不想让它生成方法toString()和构造函数,可使用注解@EqualsAndHashCode。同样,如果你只想让Groovy生成方法toString()的实现,可使用注解@ToString。最后,如果你只想让Groovy生成构造函数,可使用注解@TupleConstructor。在这些注解中,有些有可选的参数,让你能够实现更细致的控制。本书不会对这个主题做更深入的讨论,详情请参阅文档。

11.2 Groovy语言 - 图9 前述所有注解都必须从groovy.transform包中导入后才能使用。

  • 创建不可变类

正如你在前几章看到的,不可变类是函数式编程的基石。Groovy提供了很多可帮助你以函数式编程风格编写代码的功能。众所周知,可变类是bug的温床,因此即便你不打算使用Groovy进行大量的函数式编程,最好也应将这些类声明为不可变的。要将类声明为不可变的,可使用groovy.transform包中的注解@Immutable

  1. import groovy.transform.Immutable
  2. @Immutable
  3. class Person {
  4. String name
  5. }

与注解@Canonical类似,这个注解也让编译器生成如下元素:

  • 不接受任何参数的构造函数;
  • 将每个属性都作为参数的构造函数;
  • 方法hashCode()的实现;
  • 方法equals()的实现;
  • 方法toString()的实现。
    除让编译器生成上述元素外,@Immutable还:

  • 将类声明为final的,使其无法被其他类继承。

  • 检查所有属性的类型,看它们是否是不可变的。如果有属性不是不可变的或根本不支持不变性,@Immutable将引发异常。
  • 确保所有的设置方法都将在必要时引发异常。
    如果使用@Immutable注解的类包含一个或多个类型为自定义类的属性,这些自定义类也必须使用@Immutable注解;否则Groovy将拒绝编译这个类。我们来看一个示例:
  1. import groovy.transform.Immutable
  2. class Person {
  3. public final String name
  4. public Person(String name) { this.name = name }
  5. }
  6. @Immutable
  7. class Demo {
  8. // 不能运行
  9. Person person = new Person("test")
  10. }
  11. def d = new Demo()

Groovy将拒绝运行上述代码,因为它不知道Person类是否是不可变的:

  1. java.lang.RuntimeException: @Immutable processor doesn't know how to handle
  2. field 'person' of type 'Person' while constructing class Demo.

要让这些代码能够运行,请将@Immutable替换为如下代码行:

  1. @Immutable(knownImmutableClasses=[Person])
  2. class Demo {
  3. ...
  4. }

这样,Groovy将相信你的直觉,认为将这个类标记为不变的是安全的。还可指定属性名(而不是类型):

  1. @Immutable(knownImmutables=["person"])

这在你想使用def person = new Person()(而不是Person person = new Person())时很有用。

11.2 Groovy语言 - 图10 当然,如果直接给Person类本身添加注解@Immutable,代码将更容易理解和维护。这样做后,Groovy将知道Person类是不变的,因此你无需手动调整Demo类。