5 分钟的 Java 转 Groovy 教程

1. 安装

安装 Groovy 本身非常简单。对于 Windows 端的用户,将 SDK Bundle 压缩包解压到磁盘的任意路径下,然后像配置 Java 一样去配置 Groovy 的 GROOVY_PATH 和 PATH 变量,Groovy 就算安装完成了。至于 IDE 的选择,笔者仍然选择使用 IDEA IntelliJ,去 plugins 那里搜一搜 Groovy 的插件,然后安装即可。

重点来了。笔者选择安装的版本是 GDK 3.0.8 ( 官方说这是最新的稳定发行版本),它最高支持到 JDK 1.8 ,在更高的版本运行 Groovy 会报错1。这不是我们因为操作疏忽引发的错误,这主要和 JDK 本身的变动有关系。如果要在高版本的 JDK 下运行 Groovy 脚本,则需要在项目中将缺失的依赖项补充上 ( 假设正在 Maven 项目中使用它 ):

<dependencies>
    <dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
        <version>2.3.1</version>
    </dependency>

    <dependency>
        <groupId>org.glassfish.jaxb</groupId>
        <artifactId>jaxb-runtime</artifactId>
        <version>2.3.1</version>
    </dependency>
</dependencies>
复制代码

如果你需要使用 Java 和 Groovy 混合开发一些项目,那么 Maven 应该同时配置一个编译插件以及保证 Groovy 能够正常编译的最小依赖包 ( 下方的 groovy-all )。详情参考这篇知乎链接

<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>3.0.3</version>
    <type>pom</type>
</dependency>
复制代码

2. Hello!初来乍到

首先,很难用 “编译型还是解释型” 来区分 Groovy 和 Java,因为两者都需要 javac or groovyc 将源码翻译成二进制码,然后交给 JVM 解释执行。这样看的话,Java 和 Groovy 应该都算 “编译兼解释型” 语言。两者的主要区别是:Java 是典型的静态语言 ( 所有的数据都在编译期间就被确定 ),而 Groovy 可以做到 “动态分发”,同时也支持静态编译。

下面的细节有助于我们快速从 Java 过渡 Groovy。

2.1 Groovy as Script

得益于 Groovy 的简练语法(其实几乎只要是个新颖的编程语言就要比 Java 简洁得多,因此 “简洁” 其实不应该再算是 Groovy 的 Feature)和动态特性,使得 Groovy 可以轻松地和系统进程进行交互:

// 用 groovy 去执行 "groovy -v".
println("groovy -v".execute().text)
复制代码

execute() 方法可以将这个字符串视作是一个命令交给系统去执行,.text 可以获取该命令在系统下的执行结果。下面演示了在 Linux 和 Windows 系统当中,如何通过 .groovy 脚本实现 “浏览当前目录(即执行者 .groovy 所在的那个目录下)的内容”:

// Linux 系统
println("ls -l")

// Windows 系统
println("cmd /C dir")
复制代码

注意,ls 是 Linux 系统中可以直接运行的程序,但是 dir 在 Widows 系统中仅仅是 cmd 命令行解释器当中定义的一条命令,所以在这里补充了额外的前缀 cmd /C。一段 Groovy 代码还可以即时调用另一个文件存储的 Groovy 代码。比如:

evaluate(new File("C:\\Users\\i\\IdeaProjects\\GroovyHello\\src\\HelloWorld.groovy"))
// 在另一个 ./HelloWorld.groovy 脚本中,我们在那个文件里仅使用了一句 print 'hello World! -by groovy' 令其输出一段话。
复制代码

因此,Groovy 被称之为是 “JVM 上的脚本语言”,这名副其实。

2.2 编写 Groovy 逻辑的两种风格

.groovy 文件内,可以不声明任何类而直接在文件顶级层次编写代码逻辑 (笔者刚才就是这样做的)。不过这样的话,就不能在文件的顶级层次再声明一个和文件同名的类,否则编译器会给出 there is a synthetic class generated for script code 的错误。

// 假定这段代码出现在 Obj.groovy 源文件中 
class Obj{
    //...
}

// 这个方法调用和类声明一样都在文件的 '顶级' 位置。
// We don't need 'System.out.println(...)' anymore.
print('hello groovy')
复制代码

从编译角度来看这可以理解,因为 .groovy 文件被编译成 .class 文件并执行时,编译器实际上会为其生成一个合成类,而正是这一步导致了冲突发生:我们刚定义的类名和它重复了。

实际上,如果 .groovy 文件内部出现了和文件同名的类,则意味着这个 .groovy 文件会被视作是一段 “用 Groovy 方言编写的 Java 代码”,一般它也就不再作为脚本使用,而是变成一个 “普通的类” ( IDEA 称它是一个 Groovy Class) 。这么做的一个直接后果是,我们不能够在文件的顶级层次直接编写代码逻辑。

// 这段代码出现在 Obj.groovy 源文件中。
// 它相当于是 .java 文件中的 public class 定义.
class Obj{
    // 就像在写 Java 代码一样!
    // 在 Groovy 中,public 关键字其实是多余的,默认情况下所有的声明就是 public。
	public static void main(String[] args){
         // 在 Groovy 中,字符串还可以使用 "",'',""" """,''' ''',包裹,具体区别见后文。
		print('hello groovy')
	}
}

// 它相当于是 .java 文件中的非 public class 定义.
class OtherObj{}

// 代码不能再出现在文件的 '顶层' 位置,因为我们在 Java 开发时就不是这样做的。
print ('oops')
复制代码

2.3 异常处理

Java 总是要我们在第一时间处理受检异常,否则傲娇的 javac 编译器就拒绝执行。比如:

public class Test {
    public static void main(String[] args) throws FileNotFoundException {
        File file = new File("src/HelloWorld.groovy");
        // FileInputStream 抛出受检异常,因此调用它的函数要么继续 throws 到上级 (当然,主程序 throws 异常没什么意义),
        // 要么就通过 try-catch 内部解决掉它。
        FileInputStream fileInputStream = new FileInputStream(file);
    }
}
复制代码

而 Groovy 对异常处理的写法更为宽松:如果没有在该代码块内通过 try-catch 处理异常,那么该异常就会自动地向上级抛出,且无需在函数声明中使用 throws 主动定义它们。下面是 Groovy 代码:

// 即使没有声明 throws,也没有定义 try-catch, groovyc 仍然会正常执行。
File file = new File('HelloWorld.groovy')
FileInputStream fileInputStream = new FileInputStream(file)
复制代码

2.4 简洁的 “非空则调用” 语法

为了避免调用某个空指针的方法,在 Java 代码中,我们通常要包裹一层 if 语句块:

String maybeNull = "I'm Java";
if(maybeNull != null){System.out.println(nullString.length());}
复制代码

这一长串逻辑在 Groovy 当中可以直接使用一个 ?. 操作符解决:

String maybeNull = 'I\'m groovy'
print(maybeNull?.length())
复制代码

2.5 GString

在 Groovy 中,短字符串可以使用 '' 或者 "" 表示,而需要跨行的长字符串则通常使用 ''' ''' 或者 """ """ 。被双引号包括的字符串又被称为 GString,和原生的 String 相比,它支持在字符串内部使用 ${} 做占位符 ( 类似 printf ),避免了手工的 String 字符串拼接。

name = 'Wangfang'
age = '18'

// 占位符拼接的用法
print("my name is ${name},my age is ${age}.")
复制代码

2.6 精简的 JavaBean

在 Groovy 当中,编译器总是自动在底层为属性生成对应的 Set 和 Get 方法:

class Student_ {
    String name
    Integer age
    
    Student_(String name,Integer age){
        this.name = name
        this.age = age
    }
}

Student_ s = new Student_()
// 这个 getXXX 方法是由编译器生成的。
s.getName()
复制代码

如果希望某个属性在对象被构造之后就不可变,则需使用 final 关键字,编译器将不会主动地为其生成 Set 方法 ( 意味着该属性是只读的 ) 。另外,属性可以不主动声明类型,此时原本的类型被 def 关键字替代。

class Student_{
	final name
	def age
	Student_(name,age){
		this.name = name 
		this.age = age
	}
}
复制代码

对于未主动声明类型的属性,其本质上属于 Object 对象,这不利于对该属性的后续操作。想要解决这个问题,不妨在构造器中留下一些线索,以便于编译器能够 “推导” 出目标类型 ( Groovy 总是通过变量的赋值来推断这个变量的实际类型 )。

class Student_{    
    final name
    def age

    // 使得 name 和 age 属性的实际类型可以被推导 
    Student_(String name, Integer age){
        this.name = name
        this.age = age
    }
}

s = new Student_('Wang Fang',23)
复制代码

如果一个属性被声明为了 private,则编译器不会再自动地为该属性声明 Get 和 Set 方法。

class Student_{    
    final name
    private age

    // 使得 name 和 age 属性的实际类型可以被推导 
    Student_(String name, Integer age){
        this.name = name
        this.age = age
    }
}

s = new Student('Wang Fang',23)

//报错
s.getName()
复制代码

2.7 或许无需手动创建构造器

对于上述的 Student_ 类而言,它可能需要有 4 个构造器:无参构造器,仅附带 name 属性的构造器,仅附带 age 属性的构造器,完整的构造器。Groovy 可以让我们仅通过一个 Map 实现灵活的对象创建,并且不需要再手动地补充构造器写法:

class Student_{
    String name
    Integer age
}

// 没有实现 Student_(name,age) 构造器,但是可以直接使用
stu1 = new Student_(name: "Wang Fang",age: 12)

// 同样,我们也没有手动实现 Student_(name) 构造器。
stu2 = new Student_(name:"Wang Fang")
复制代码

在些参数列表中,我们传入的其实是一整个 Map。里面的每一个 k:v 都表示了一个键值对。k 对应了这个类当中每个属性名,而 v 则为这些属性赋值。但是,我们不能这样做:

stu1 = new Student_("Wang Fang",12)
stu2 = new Student_("Wang Fang")
复制代码

除非手动地补充上对应的构造函数。

2.8 方法中的可选形参

Java 不支持可选形参,在调用方法时,每个参数必须严格赋值。而 Groovy 则有所不同:在方法 ( 或函数 ) 参数列表内,可以提前为最后一个参数设定默认值,那么在调用该方法时,最后一个参数可以被省略。

def add(Integer arg,Integer implicit = 10){arg + implicit}

// 11
print(add(1))
// 3 
print(add(1,2))
复制代码

这个例子还展现了其它细节:至少,Groovy 的方法 ( 函数 ) 不要求显示地添加 return 关键字,它总是默认返回函数体内最后一个调用的结果值。然后,这个函数的返回值类型是显然可以推断的,因此这里也可使用 def 关键字替换掉函数的返回值声明。

2.9 多重赋值

如果方法 ( 函数 ) 返回的是一个数组,那么 Groovy 支持使用多个变量接收数组内的元素内容。比如:

def swap(x, y) { return [y, x] }

Integer a, b
a = 10
b = 50

// 通过多重赋值实现了两数交换
(a, b) = swap(a, b)
print("a=$a,b=$b")
复制代码

利用这个特点,Groovy 的方法 (函数) 可以返回多个值。其它支持这么做的编程语言还有 Go,Scala ( 通过包装成元组来实现 ) 等。当接收变量的个数和实际的返回值个数不匹配时,Groovy 会这样做:

  1. 如果接收的变量更多,那么会将没有赋值的变量赋为 null 。
  2. 如果返回值更多,那么多余的返回值会被丢弃。

当然,Groovy 也的确提供了元组,这个写法对于一些 Scala 程序员绝对不陌生:

Tuple2<Integer,Integer> swap(Integer a,Integer b){
    return new Tuple2<Integer,Integer>(b,a)
}

a = 10
b = 20

(a, b) = swap(a, b)
print("a=${a},b=${b}")
复制代码

2.10 接口实现

假定有这样一个单方法接口:

interface Calculator<T>{
    T add(T a,T b)
}
复制代码

Java 可能要这样实现:

Calculator<Integer> calculator = new Calculator<Integer>() {
    @Override
    public Integer add(Integer a, Integer b) {
        return a + b;
    }
};
复制代码

在 Java 8 之后,匿名实现的写法终于变得更简练了亿些,但遗憾的是,Lambda 表达式只能用于单方法接口。

Calculator<Integer> calculator = (a, b) -> a + b;
复制代码

Groovy 给出了与众不同的解决思路:首先给出 Lambda 表达式的语法块,这个语法块被 {} 包裹,在 Groovy 中它被称之为闭包;然后通过 as 关键字将这个闭包声明为是对某一接口的实现2

// 多亏类型推导的存在,我们不需要把 Calculator<Integer> 重新抄写一遍 .....
def a = {a,b ->return a+b} as Calculator<Integer>
复制代码

如果要实现多方法接口,那么就将多个闭包装入到一个 Map 当中,使用 k 来标注每个闭包实现的是哪个方法:

interface Calculator<T> {
    T add(T a, T b)
    T sub(T a, T b)
}

def cal = [
        add: { a, b -> a + b },
        sub: { a, b -> a - b }
] as Calculator<Integer>

def c = cal.sub(1,2)
print(c)
复制代码

Groovy 从未强制实现一个接口的所有方法:如果某些方法确实用不到,那就没有必要将对应的闭包实现放入 Map 中。值得注意的是,如果调用了没有实现的接口方法,那么程序就会抛出亲切的 NullPointerException 异常。

2.11 布尔求值

在 If 语句的条件部分,Java 强制要求传入一个计算好的布尔值,否则就报错。

int a = 10
// :)     => if(a != 0)...
// :(     => if(a)...
复制代码

Groovy 的处理则更加优雅一些,当传入的值不是纯粹的布尔值时,Groovy 会基于传入的类型进行一些合理的推断,而不是直接报错,参见下方的表格:

类型 何时为真
Boolean 值是 true
Collection 集合本身不是 null,且内部有元素
Character 值不为 0
CharSequence 长度大于 0
Enumeration Has More Enumerations 为 True
Iterator hasNext() 为 True
Number Double 值不为 0
Map 映射本身不是 null,且映射内部不为空
Matcher 至少有一个匹配
Object[] 长度大于 0
其它类型 引用不为 null

在大部分情况下,直接向 if 条件部分传入一个值都是为了判断它是否为空。如果要基于该值是否为空来决定是否执行一系列动作,可以考虑使用前文提到的 ?. 操作符简化代码。

2.12 运算符重载

Groovy 预留了一些方法名称,这些方法意味着对操作符进行重载3

Operator Method
a + b a.plus(b)
a – b a.minus(b)
a * b a.multiply(b)
a ** b a.power(b)
a / b a.div(b)
a % b a.mod(b)
a | b a.or(b)
a & b a.and(b)
a ^ b a.xor(b)
a++ or ++a a.next()
a– or –a a.previous()
a[b] a.getAt(b)
a[b] = c a.putAt(b, c)
a << b a.leftShift(b)
a >> b a.rightShift(b)
switch(a) { case(b) : } b.isCase(a)
~a a.bitwiseNegate()
-a a.negative()
+a a.positive()

而这些操作符在遇到 null 时不会抛出空指针异常:

Operator Method
a == b a.equals(b) or a.compareTo(b) == 0 **
a != b ! a.equals(b)
a <=> b a.compareTo(b)
a > b a.compareTo(b) > 0
a >= b a.compareTo(b) >= 0
a < b a.compareTo(b) < 0
a <= b a.compareTo(b) <= 0

举个例子:在程序中定义复数类,然后定义两个复制之和是实部和虚部的分别加和:

class ComplexNumber {
    Integer real
    Integer imaginary

    //plus 方法对应 + 操作符
    def plus(ComplexNumber other) {
        new ComplexNumber(real: this.real + other.real,imaginary: this.imaginary+other.imaginary)
    }

    @Override
    String toString() { "${real} + ${imaginary}i"}
}

// 注意,这个写法相当于 println(...)
// 内部相当于是调用了 new ComplexNumber(balabala).plus(new ComplexNumber(balabala))
println new ComplexNumber(real:3,imaginary:  2) + new ComplexNumber(real:3,imaginary:1)
复制代码

但相比 Scala 而言,笔者认为这种方式有点奇怪 …… 因为当我们需要这么做时,总是得翻阅一下上面的表格,然后去比对哪个操作符对应哪个方法,除非把这张表格背下来 ( 可以,但没必要 )。不过不管怎么样,有总比没有强。

2.13 for 循环

下面是一段 Java 代码演示的 for 循环,i 从 0 开始,直到 3 ( 不包括 3) 为止:

for (int i = 0; i<3 ; i++){System.out.println("java loop")}
复制代码

在 Groovy 中, 0 ~ 3 的左闭右开区间可以使用 0..2 来表示:

for (i in 0..2){println "groovy loop"}
复制代码

in 通常用于遍历 “模糊类型” 的数组。如果遍历的是确定类型的数组,还可以这样写:

String[] strings = ['java','scala','groovy','go']

//  s 必须指明是 String 类型。
//  : 替换了之前的 in 关键字。
for(String s : strings){
    print(s.length())
}

//  等价写法
for (s in strings){
    print s.length()
}
复制代码

.. 可以被视作是一个特殊的二元符号,在循环语句之外也可以单独使用它来创建一个步长为 1 的序列。

// 你可以将 .. 视作是 Integer 的一个双目运算符号,其中 n..m 会返回 [n,n+1,n+2,...m] 的序列。
// seq 是 IntRange 类型。
def seq = 0..10

// 11
print seq.size()
复制代码

2.14 关于导入

Groovy 通常的导入方式和 Java 如出一辙,并且不强制所有的 import 出现在文件的最上方。

import java.lang.Math
print Math.random()
复制代码

除此之外,Groovy 支持 import static 导入某一个类的静态方法,这样我们可以在当前命名空间当中将该静态方法直接作为一个函数来调用。如果担心命名重复,可以使用 as 关键字将该静态方法重新命名。

// 静态导入 Math 类的静态 random 方法
// as 关键字可以顺便将导入的静态方法起一个别名,通常用于简化或者避免命名冲突的目的。
import static math.random as rdm
print rdm()
复制代码

2.15 一切即闭包

Groovy 特地将 [] 留给了数组的声明:

String[] str = ['java','groovy']
复制代码

而一切 {} 代码块在 Groovy 会被视作一个闭包,闭包对于 Groovy 来说,是一个具体的 Closure<T> 类型4。( 有关 Groovy 闭包的内容笔者后续会单独说明 ) 在 Java 中,我们可以使用 {} 表示一段有独立作用域的子代码块:

{
    System.out.println("block1");
}

{
    System.out.println("block2");
}

// ()->void 函数也可以被理解成是子代码块。
Runnable runnable = () -> {
    System.out.println("block3");
};

runnable.run();
复制代码

但在 Groovy 当中,一段 {} 扩起来的闭包不能单独声明出现,除非是写成赋值的形式:

// 不能通过编译
{print "hello"}

// 编译通过,这种赋值明确地表示 {} 是一个闭包。
def c = {print "hello"}
复制代码

2.15.1 避免闭包和匿名类的冲突

如果一个函数 / 方法接收闭包作为参数,那么从语法上可以将这些闭包附着在函数调用的尾部。形象点说,一个 method({...},{...}) 语句块可以改写成 method() {...}{...} 的形式 ( 这么做有利于设计内部 DSL 语法,想想我们为什么能够在 Groovy 写出诸如 print "hello" 的句式?) :

def aspect(before, after) {
    before()
    print("method")
    after()
}

// 正常的调用方式
aspect({print "before doing..."},{print "after doing..."})


// 将闭包迁移到调用尾部,aspect() 的小括号 () 可写可不写。
aspect() {
    print("before doing...")
} {
    print("after doing...")
}
复制代码

有时,一个类的构造函数也会需要接收一个闭包,那么在这种场合可能会引发歧义:

class Aspect{
        // Closure 是 Groovy 中表示闭包的类型。它实际上还有一个泛型,该泛型指代的是该闭包的返回值。
        Aspect(Closure before){
            before()
        }
}
复制代码

按照开头的调用风格,它可以被写成这样:

// 这个写法的原意是将闭包写到构造函数的后面。
def a  = new Aspect(){
	print "create a aspect..."
}
复制代码

但对于一个 Java 程序员而言,这种写法看起来却像是在创建一个匿名对象 —— 甚至 Groovy 也会不知所措。在这种情况下,必须严格使用 () 的语法避免歧义发生。

def a = new Aspect({print "create a aspect..."})
复制代码

2.15.2 避免闭包和实例初始化器的冲突

在某些类的定义中,我们需要使用一段 {} 扩起来的代码块作为实例初始化器

class Apple{

    String from = "China"

    // 我们不认为它是闭包,而是实例初始化器
    {
        print("这段代码先于 Apple 的构造函数去执行")
    }
}
复制代码

然而 Groovy 却会把字符串 "China" 和它认为的 “闭包” {...} 视作是一个整体,而导致运行时出错。解决办法有两种:要么将实例初始化器移动到内部声明的最上方,要么就显示地使用 ; 分号将两者分隔开:

// 解决方法1,推荐
class Apple{

    {
        print("")
    }
    
    String from = "China"
}

// 解决方法 2,不太推荐,因为这种写法容易让人混淆实例初始化器和普通闭包的声明。
class Apple{

    String from = "China";

    {
        print("")
    }
}
复制代码

2.16 强力注解

这里或许有一些官方提供的注解帮助快速开发,它们绝大部分都是来自于 groovy.lang 包,这意味着不需要通过 import 关键字额外地导入外部依赖:

2.16.1 @Canonical 替代 toString

假如希望打印一个类信息,又不想自己生成 toString() 方法,则可以使用 @Canonical 注解。该注解有额外的 excludes 选项:允许我们忽略一些属性。

@Canonical
// 如果不想打印 id 和 score,可以:
// @Canonical(excludes="id,score")
class Student {
    Integer id
    String name
    Integer age
    String major
    Integer score
}

// 如果没有此注解,打印的则是 Student@Hashcode
// 如果有注解,打印的则是 Student(1,"Wang Fang",20,"CS","score")
print new Student(id: 1,name:"Wang Fang",age: 20,major: "CS",score: 90.0d)
复制代码

2.16.2 @Delegate 实现委托

使用 @Delegate 注解,在 Groovy 中实现方法委托非常容易。委托是继承以外的另一种代码复用的思路。在下面的代码块中,Manager 通过注解将 work() 方法委托给了内部的 worker 属性:

class Worker{
    void work(){
        print("worker is working exactly.")
    }
}

// Manager 获得了 Worker 的公开方法,尽管 worker 属性本身是 private.
class Manager{
    @Delegate private Worker worker = new Worker()
}

// 检查 Manager 实例有没有 work 方法,没有就去委托 worker 执行此方法。
new Manager().work()
复制代码

2.16.3 @Immutable 不可变对象*

不可变的对象天生就是线程安全的。想要创建一个不可变对象,需要限制它的类属性全部是 final ,一旦属性被初始化之后就不可以再被改变。@Immutable 注解可以提供一个便捷的解决方案:

@Immutable
class Student_{
    String id
    String name
}

def s = new Student_(id:"0001",name:"Wang Fang")

print s
复制代码

和其它注解不同,它来自 groovy.transform 包。笔者在使用该注解的时候曾遇到一些奇怪的问题,IDEA 似乎不能很好的识别该注解,并进一步引发代码无法粘贴,错误地弹出警告,代码提示消失等 Bug。

2.16.4 @Lazy 延迟加载类成员

懒加载是大部分新兴语言都支持的特性。在 Groovy 中,它通过注解来实现,注意,该注解只能用于类成员

class Connection{
    
    // 加载 Connection 需要 1 秒的时间
    Connection(){
        Thread.sleep(1000)
        print "Connection 实例初始化完毕"
    }
}

class Pool{
    
    // 由于代码没有调用 conn ,因此实际上 new Connection() 并没有真正执行
    @Lazy def conn = new Connection()
    Pool(){
        print "Pool 实例初始化完毕"
    }
}

def pool = new Pool();
复制代码

对于懒加载的成员只有在第一次被调用时才会被初始化,并且 Groovy 内部通过 voaltitle 关键字保证这个创建的过程是线程安全的。

2.16.5 @Newify 注解

该注解的功能有点类似于 Scala 语言当中的 apply 方法,允许我们在创建新对象的时候忽略掉 new 关键字 ( 这个特性也有助于设计 DSL )。该注解可用在类声明和方法声明,也可以用在单独的变量赋值语句上:

class Student{
    String id
    String name
}

class Teacher{
    String id
    String name
}

@Newify(Student)
def getStudent(){
    // 在函数内部创建 Student 时,可以省略掉 new 关键字。
    Student(id:"0001",name: "Wang Fang")
}

// 多个类型使用数组的形式排列。
@Newify([Student,Teacher])
static def getStudentAndTeacher(){
    [Student(id:"0001",name:"Wang Fang"),Teacher(id: "0002",name:"Cheng Yu")]
}
复制代码

2.16.6 @Singleton 单例模式

在 Groovy 中,仅凭 @Singleton 注解就可以实现一个线程安全,并且简洁的单例模式。

// 懒加载的单例模式,lazy 项是可选的。
@Singleton(lazy = true)
class TheUnique{
    {
        println "created only once"
    }
}

// 通过 .instance 调用这个单例对象。
TheUnique.instance
复制代码

单例模式可以选择懒汉式加载,仅需在注解的 lazy 选项中设置为 true 即可。

2.17 注意 Groovy 的 == 符号

在 Java 中,== 可以比较两个基本数据类型的值,或者比较两个引用类型的 HashCode。而 .equals() 方法如何比较则取决于开发者制定的规则:在什么都不做的情况下,.equals 方法和 == 等价。

对于一些常用类型,Java 已经制定好了 .equals() 方法的比较规则。就 String 而言,它的 .equals() 实现首先就是通过 == 符号判断两个字符串的引用是否相同,然后判断两个字符串的长度是否相同,最后再按位判断每个位置的字符是否相同。

而在 Groovy 当中,这两者的混乱程度有所加剧:Groovy 的 == 相当于是 Java 的 .equals() 方法或者是 compareTo() 方法 (见运算符重载的那个表格),而 Java 原始的 == 语义在 Groovy 中变成了 is() 方法。

str1 = "111"
str2 = "222"

// 相当于是 Java 语义中的 str1 == str2
str1.is(str2)

// 相当于是 Java 语义中的 str1.equals(str2)
str1 == str2
复制代码

如果比较的类实现了 Compareble 接口,那么 == 的语义优先会选择 compareTo() 方法而非 equals() 方法。

3. 参考链接

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享