安装 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 项目中使用它 ):
1 | xml复制代码<dependencies> |
如果你需要使用 Java 和 Groovy 混合开发一些项目,那么 Maven 应该同时配置一个编译插件以及保证 Groovy 能够正常编译的最小依赖包 ( 下方的 groovy-all )。详情参考这篇知乎链接。
1 | xml复制代码<dependency> |
首先,很难用 “编译型还是解释型” 来区分 Groovy 和 Java,因为两者都需要 javac
or groovyc
将源码翻译成二进制码,然后交给 JVM 解释执行。这样看的话,Java 和 Groovy 应该都算 “编译兼解释型” 语言。两者的主要区别是:Java 是典型的静态语言 ( 所有的数据都在编译期间就被确定 ),而 Groovy 可以做到 “动态分发”,同时也支持静态编译。
下面的细节有助于我们快速从 Java 过渡 Groovy。
2.1 Groovy as Script
得益于 Groovy 的简练语法(其实几乎只要是个新颖的编程语言就要比 Java 简洁得多,因此 “简洁” 其实不应该再算是 Groovy 的 Feature)和动态特性,使得 Groovy 可以轻松地和系统进程进行交互:
1 | groovy复制代码// 用 groovy 去执行 "groovy -v". |
execute()
方法可以将这个字符串视作是一个命令交给系统去执行,.text
可以获取该命令在系统下的执行结果。下面演示了在 Linux 和 Windows 系统当中,如何通过 .groovy
脚本实现 “浏览当前目录(即执行者 .groovy
所在的那个目录下)的内容”:
1 | groovy复制代码// Linux 系统 |
注意,ls
是 Linux 系统中可以直接运行的程序,但是 dir
在 Widows 系统中仅仅是 cmd
命令行解释器当中定义的一条命令,所以在这里补充了额外的前缀 cmd /C
。一段 Groovy 代码还可以即时调用另一个文件存储的 Groovy 代码。比如:
1 | groovy复制代码evaluate(new File("C:\\Users\\i\\IdeaProjects\\GroovyHello\\src\\HelloWorld.groovy")) |
因此,Groovy 被称之为是 “JVM 上的脚本语言”,这名副其实。
2.2 编写 Groovy 逻辑的两种风格
在 .groovy
文件内,可以不声明任何类而直接在文件顶级层次编写代码逻辑 (笔者刚才就是这样做的)。不过这样的话,就不能在文件的顶级层次再声明一个和文件同名的类,否则编译器会给出 there is a synthetic class generated for script code
的错误。
1 | groovy复制代码// 假定这段代码出现在 Obj.groovy 源文件中 |
从编译角度来看这可以理解,因为 .groovy
文件被编译成 .class
文件并执行时,编译器实际上会为其生成一个合成类,而正是这一步导致了冲突发生:我们刚定义的类名和它重复了。
实际上,如果 .groovy
文件内部出现了和文件同名的类,则意味着这个 .groovy
文件会被视作是一段 “用 Groovy 方言编写的 Java 代码”,一般它也就不再作为脚本使用,而是变成一个 “普通的类” ( IDEA 称它是一个 Groovy Class) 。这么做的一个直接后果是,我们不能够在文件的顶级层次直接编写代码逻辑。
1 | groovy复制代码// 这段代码出现在 Obj.groovy 源文件中。 |
2.3 异常处理
Java 总是要我们在第一时间处理受检异常,否则傲娇的 javac
编译器就拒绝执行。比如:
1 | java复制代码public class Test { |
而 Groovy 对异常处理的写法更为宽松:如果没有在该代码块内通过 try-catch 处理异常,那么该异常就会自动地向上级抛出,且无需在函数声明中使用 throws
主动定义它们。下面是 Groovy 代码:
1 | groovy复制代码// 即使没有声明 throws,也没有定义 try-catch, groovyc 仍然会正常执行。 |
2.4 简洁的 “非空则调用” 语法
为了避免调用某个空指针的方法,在 Java 代码中,我们通常要包裹一层 if 语句块:
1 | groovy复制代码String maybeNull = "I'm Java"; |
这一长串逻辑在 Groovy 当中可以直接使用一个 ?.
操作符解决:
1 | groovy复制代码String maybeNull = 'I\'m groovy' |
2.5 GString
在 Groovy 中,短字符串可以使用 ''
或者 ""
表示,而需要跨行的长字符串则通常使用 ''' '''
或者 """ """
。被双引号包括的字符串又被称为 GString,和原生的 String 相比,它支持在字符串内部使用 ${}
做占位符 ( 类似 printf
),避免了手工的 String 字符串拼接。
1 | groovy复制代码name = 'Wangfang' |
2.6 精简的 JavaBean
在 Groovy 当中,编译器总是自动在底层为属性生成对应的 Set 和 Get 方法:
1 | groovy复制代码class Student_ { |
如果希望某个属性在对象被构造之后就不可变,则需使用 final
关键字,编译器将不会主动地为其生成 Set 方法 ( 意味着该属性是只读的 ) 。另外,属性可以不主动声明类型,此时原本的类型被 def
关键字替代。
1 | groovy复制代码class Student_{ |
对于未主动声明类型的属性,其本质上属于 Object 对象,这不利于对该属性的后续操作。想要解决这个问题,不妨在构造器中留下一些线索,以便于编译器能够 “推导” 出目标类型 ( Groovy 总是通过变量的赋值来推断这个变量的实际类型 )。
1 | groovy复制代码class Student_{ |
如果一个属性被声明为了 private
,则编译器不会再自动地为该属性声明 Get 和 Set 方法。
1 | groovy复制代码class Student_{ |
2.7 或许无需手动创建构造器
对于上述的 Student_ 类而言,它可能需要有 4 个构造器:无参构造器,仅附带 name 属性的构造器,仅附带 age 属性的构造器,完整的构造器。Groovy 可以让我们仅通过一个 Map 实现灵活的对象创建,并且不需要再手动地补充构造器写法:
1 | groovy复制代码class Student_{ |
在些参数列表中,我们传入的其实是一整个 Map。里面的每一个 k:v
都表示了一个键值对。k
对应了这个类当中每个属性名,而 v
则为这些属性赋值。但是,我们不能这样做:
1 | groovy复制代码stu1 = new Student_("Wang Fang",12) |
除非手动地补充上对应的构造函数。
2.8 方法中的可选形参
Java 不支持可选形参,在调用方法时,每个参数必须严格赋值。而 Groovy 则有所不同:在方法 ( 或函数 ) 参数列表内,可以提前为最后一个参数设定默认值,那么在调用该方法时,最后一个参数可以被省略。
1 | groovy复制代码def add(Integer arg,Integer implicit = 10){arg + implicit} |
这个例子还展现了其它细节:至少,Groovy 的方法 ( 函数 ) 不要求显示地添加 return
关键字,它总是默认返回函数体内最后一个调用的结果值。然后,这个函数的返回值类型是显然可以推断的,因此这里也可使用 def
关键字替换掉函数的返回值声明。
2.9 多重赋值
如果方法 ( 函数 ) 返回的是一个数组,那么 Groovy 支持使用多个变量接收数组内的元素内容。比如:
1 | groovy复制代码def swap(x, y) { return [y, x] } |
利用这个特点,Groovy 的方法 (函数) 可以返回多个值。其它支持这么做的编程语言还有 Go,Scala ( 通过包装成元组来实现 ) 等。当接收变量的个数和实际的返回值个数不匹配时,Groovy 会这样做:
- 如果接收的变量更多,那么会将没有赋值的变量赋为 null 。
- 如果返回值更多,那么多余的返回值会被丢弃。
当然,Groovy 也的确提供了元组,这个写法对于一些 Scala 程序员绝对不陌生:
1 | groovy复制代码Tuple2<Integer,Integer> swap(Integer a,Integer b){ |
2.10 接口实现
假定有这样一个单方法接口:
1 | groovy复制代码interface Calculator<T>{ |
Java 可能要这样实现:
1 | java复制代码Calculator<Integer> calculator = new Calculator<Integer>() { |
在 Java 8 之后,匿名实现的写法终于变得更简练了亿些,但遗憾的是,Lambda 表达式只能用于单方法接口。
1 | css复制代码Calculator<Integer> calculator = (a, b) -> a + b; |
Groovy 给出了与众不同的解决思路:首先给出 Lambda 表达式的语法块,这个语法块被 {}
包裹,在 Groovy 中它被称之为闭包;然后通过 as
关键字将这个闭包声明为是对某一接口的实现2。
1 | groovy复制代码// 多亏类型推导的存在,我们不需要把 Calculator<Integer> 重新抄写一遍 ..... |
如果要实现多方法接口,那么就将多个闭包装入到一个 Map 当中,使用 k
来标注每个闭包实现的是哪个方法:
1 | groovy复制代码interface Calculator<T> { |
Groovy 从未强制实现一个接口的所有方法:如果某些方法确实用不到,那就没有必要将对应的闭包实现放入 Map 中。值得注意的是,如果调用了没有实现的接口方法,那么程序就会抛出亲切的 NullPointerException
异常。
2.11 布尔求值
在 If 语句的条件部分,Java 强制要求传入一个计算好的布尔值,否则就报错。
1 | java复制代码int a = 10 |
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 & 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 |
举个例子:在程序中定义复数类,然后定义两个复制之和是实部和虚部的分别加和:
1 | groovy复制代码class ComplexNumber { |
但相比 Scala 而言,笔者认为这种方式有点奇怪 …… 因为当我们需要这么做时,总是得翻阅一下上面的表格,然后去比对哪个操作符对应哪个方法,除非把这张表格背下来 ( 可以,但没必要 )。不过不管怎么样,有总比没有强。
2.13 for 循环
下面是一段 Java 代码演示的 for 循环,i 从 0 开始,直到 3 ( 不包括 3) 为止:
1 | java复制代码for (int i = 0; i<3 ; i++){System.out.println("java loop")} |
在 Groovy 中, 0 ~ 3 的左闭右开区间可以使用 0..2
来表示:
1 | groovy复制代码for (i in 0..2){println "groovy loop"} |
in
通常用于遍历 “模糊类型” 的数组。如果遍历的是确定类型的数组,还可以这样写:
1 | groovy复制代码String[] strings = ['java','scala','groovy','go'] |
..
可以被视作是一个特殊的二元符号,在循环语句之外也可以单独使用它来创建一个步长为 1 的序列。
1 | groovy复制代码// 你可以将 .. 视作是 Integer 的一个双目运算符号,其中 n..m 会返回 [n,n+1,n+2,...m] 的序列。 |
2.14 关于导入
Groovy 通常的导入方式和 Java 如出一辙,并且不强制所有的 import
出现在文件的最上方。
1 | groovy复制代码import java.lang.Math |
除此之外,Groovy 支持 import static
导入某一个类的静态方法,这样我们可以在当前命名空间当中将该静态方法直接作为一个函数来调用。如果担心命名重复,可以使用 as
关键字将该静态方法重新命名。
1 | groovy复制代码// 静态导入 Math 类的静态 random 方法 |
2.15 一切即闭包
Groovy 特地将 []
留给了数组的声明:
1 | groovy复制代码String[] str = ['java','groovy'] |
而一切 {}
代码块在 Groovy 会被视作一个闭包,闭包对于 Groovy 来说,是一个具体的 Closure<T>
类型4。( 有关 Groovy 闭包的内容笔者后续会单独说明 ) 在 Java 中,我们可以使用 {}
表示一段有独立作用域的子代码块:
1 | java复制代码{ |
但在 Groovy 当中,一段 {}
扩起来的闭包不能单独声明出现,除非是写成赋值的形式:
1 | groovy复制代码// 不能通过编译 |
2.15.1 避免闭包和匿名类的冲突
如果一个函数 / 方法接收闭包作为参数,那么从语法上可以将这些闭包附着在函数调用的尾部。形象点说,一个 method({...},{...})
语句块可以改写成 method() {...}{...}
的形式 ( 这么做有利于设计内部 DSL 语法,想想我们为什么能够在 Groovy 写出诸如 print "hello"
的句式?) :
1 | groovy复制代码def aspect(before, after) { |
有时,一个类的构造函数也会需要接收一个闭包,那么在这种场合可能会引发歧义:
1 | groovy复制代码class Aspect{ |
按照开头的调用风格,它可以被写成这样:
1 | groovy复制代码// 这个写法的原意是将闭包写到构造函数的后面。 |
但对于一个 Java 程序员而言,这种写法看起来却像是在创建一个匿名对象 —— 甚至 Groovy 也会不知所措。在这种情况下,必须严格使用 ()
的语法避免歧义发生。
1 | groovy复制代码def a = new Aspect({print "create a aspect..."}) |
2.15.2 避免闭包和实例初始化器的冲突
在某些类的定义中,我们需要使用一段 {}
扩起来的代码块作为实例初始化器:
1 | groovy复制代码class Apple{ |
然而 Groovy 却会把字符串 "China"
和它认为的 “闭包” {...}
视作是一个整体,而导致运行时出错。解决办法有两种:要么将实例初始化器移动到内部声明的最上方,要么就显示地使用 ;
分号将两者分隔开:
1 | groovy复制代码// 解决方法1,推荐 |
2.16 强力注解
这里或许有一些官方提供的注解帮助快速开发,它们绝大部分都是来自于 groovy.lang
包,这意味着不需要通过 import
关键字额外地导入外部依赖:
2.16.1 @Canonical 替代 toString
假如希望打印一个类信息,又不想自己生成 toString()
方法,则可以使用 @Canonical
注解。该注解有额外的 excludes
选项:允许我们忽略一些属性。
1 | groovy复制代码@Canonical |
2.16.2 @Delegate 实现委托
使用 @Delegate
注解,在 Groovy 中实现方法委托非常容易。委托是继承以外的另一种代码复用的思路。在下面的代码块中,Manager 通过注解将 work()
方法委托给了内部的 worker 属性:
1 | groovy复制代码class Worker{ |
2.16.3 @Immutable 不可变对象*
不可变的对象天生就是线程安全的。想要创建一个不可变对象,需要限制它的类属性全部是 final
,一旦属性被初始化之后就不可以再被改变。@Immutable
注解可以提供一个便捷的解决方案:
1 | groovy复制代码@Immutable |
和其它注解不同,它来自 groovy.transform
包。笔者在使用该注解的时候曾遇到一些奇怪的问题,IDEA 似乎不能很好的识别该注解,并进一步引发代码无法粘贴,错误地弹出警告,代码提示消失等 Bug。
2.16.4 @Lazy 延迟加载类成员
懒加载是大部分新兴语言都支持的特性。在 Groovy 中,它通过注解来实现,注意,该注解只能用于类成员。
1 | groovy复制代码class Connection{ |
对于懒加载的成员只有在第一次被调用时才会被初始化,并且 Groovy 内部通过 voaltitle 关键字保证这个创建的过程是线程安全的。
2.16.5 @Newify 注解
该注解的功能有点类似于 Scala 语言当中的 apply 方法,允许我们在创建新对象的时候忽略掉 new
关键字 ( 这个特性也有助于设计 DSL )。该注解可用在类声明和方法声明,也可以用在单独的变量赋值语句上:
1 | groovy复制代码class Student{ |
2.16.6 @Singleton 单例模式
在 Groovy 中,仅凭 @Singleton
注解就可以实现一个线程安全,并且简洁的单例模式。
1 | groovy复制代码// 懒加载的单例模式,lazy 项是可选的。 |
单例模式可以选择懒汉式加载,仅需在注解的 lazy
选项中设置为 true
即可。
2.17 注意 Groovy 的 == 符号
在 Java 中,==
可以比较两个基本数据类型的值,或者比较两个引用类型的 HashCode。而 .equals()
方法如何比较则取决于开发者制定的规则:在什么都不做的情况下,.equals
方法和 ==
等价。
对于一些常用类型,Java 已经制定好了 .equals()
方法的比较规则。就 String 而言,它的 .equals()
实现首先就是通过 ==
符号判断两个字符串的引用是否相同,然后判断两个字符串的长度是否相同,最后再按位判断每个位置的字符是否相同。
而在 Groovy 当中,这两者的混乱程度有所加剧:Groovy 的 ==
相当于是 Java 的 .equals()
方法或者是 compareTo()
方法 (见运算符重载的那个表格),而 Java 原始的 ==
语义在 Groovy 中变成了 is()
方法。
1 | groovy复制代码str1 = "111" |
如果比较的类实现了 Compareble
接口,那么 ==
的语义优先会选择 compareTo()
方法而非 equals()
方法。
Footnotes
- Groovy 在更高 JDK 版本使用的方法 ↩
- Groovy ‘as’ 用于实现2+接口的关键字 ↩
- Groovy:运算符重载 ↩
- 想要提前了解闭包,可以参考这篇文章:Groovy 闭包 - 简书 (jianshu.com) ↩
本文转载自: 掘金