Groovy 学习(1):语法入门
🏷️ Groovy
记录下自己看国内版 W3Cschool 上的 Groovy 教程的笔记。
初次接触到 Groovy 是在使用 Jenkins 的 pipeline 时,不过当时大多是 Ctrl C + V,没有关注过具体的语法啥的。现在则是在 Gradle 中使用 Groovy 的比较多。最近在看《函数式编程思维》,虽然也有 Java 但篇幅很少,Clojure 和 Scala 两种函数式编程语言从来没接触过,所以就先选了比较好理解的 Groovy 学习下。
Groovy 是一种基于 Java 平台的面向对象语言,和 Java 语法大体相似,可以使用现有的 Java 库。这里是基于已经了解 Java 语法的情况下学习,仅记录与 Java 不同的地方。
Hello World
class BasicSyntaxTests {
@Test
void print_hello() {
println('Hello,World!')
}
}
代码是从 start.spring.io 创建了一个 Groovy 的 SpringBoot 项目。
- Language:Groovy
- Spring Boot:2.7.8
下载之后通过 IDEA 打开并创建测试类来运行 Groovy 代码片段(由于不需要启动 SpringBoot 服务,所以不需要在测试类上添加
@SpringBootTest
注解)。
这里就是在测试类中的写法,后面也都使用类似的格式。
基础语法
分号
行尾的分号 ;
可以省略。
身份标识
通过 def
创建标识符。标识符被用来定义变量、函数或其他用户定义的变量。标识符以字母,美元或下划线开头。定义变量、函数时仍然可以使用具体的类名。
@Test
void def_variable() {
def x = 5
Assertions.assertEquals(5, x)
}
范围运算符
Groovy 支持范围的概念,并在 ..
符号的帮助下提供范围运算符的符号。范围是指定值序列的速记。
@Test
void def_range() {
def range = 5..10
Assertions.assertEquals([5, 6, 7, 8, 9, 10], range)
Assertions.assertEquals('5..10', range.toString())
Assertions.assertEquals(7, range.get(2))
Assertions.assertEquals('5,6,7,8,9,10', range.join(','))
}
其他一些范围的写法:
1..<10
- 独占范围的示例'a'..'x'
- 范围也可以由字符组成10..1
- 范围也可以按降序排列'x'..'a'
- 范围也可以由字符组成并按降序排列。
@Test
void other_ranges() {
Assertions.assertEquals([1, 2, 3, 4], 1..<5)
Assertions.assertEquals(['a', 'b', 'c', 'd'], 'a'..'d')
Assertions.assertEquals([5, 4, 3, 2, 1], 5..1)
Assertions.assertEquals(['d', 'c', 'b', 'a'], 'd'..'a')
}
方法
- Groovy 中的方法是使用返回类型或使用
def
关键字定义的。 - 方法可以接收任意数量的参数。
- 定义参数时,不必显式定义类型。
- 可以添加修饰符,如
public
,private
和protected
。 - 默认情况下,如果未提供可见性修饰符,则该方法为
public
。 - 有返回值的方法
return
可以被省略,默认返回最后一行代码的运行结果。
void sayHello() {
println('Hello,World!')
}
方法参数
static def sum(a, b) {
a + b
}
默认参数
def someMethod(parameter1, parameter2 = 0, parameter3 = 0) {
// Method code goes here
}
“可选”类型
Groovy 是一种“可选”类型的语言,而 Java 是一种“强”类型的语言。
def
类型的变量或方法返回值,会在运行时根据分配的值来确定其类型。和 Java 中的 var
还是有很大区别的,var
只是一种简写,编译时仍然会推导变量的具体类型。
下面定义的变量 x
是 Integer
型:
def x = 5
assert x instanceof Integer
但是之后仍然可以让其赋值为其他类型的值:
@Test
void def_variable() {
def x = 5
assert x instanceof Integer
assert 5 == x
x = 1..5
assert x instanceof IntRange
}
下面 sum
方法的写法在 Java 中就只能用泛型来实现,相比较来说 Groovy 中的写法简单很多。
static def sum(a, b) {
a + b
}
@Test
void sum_int() {
Assertions.assertEquals(11, sum(5, 6))
}
@Test
void sum_long() {
Assertions.assertEquals(11L, sum(5L, 6L))
}
@Test
void sum_big_decimal() {
Assertions.assertEquals(BigDecimal.valueOf(11), sum(BigDecimal.valueOf(5), BigDecimal.valueOf(6)))
}
@Test
void sum_string() {
Assertions.assertEquals('56', sum('5', '6'))
}
@Test
void sum_range() {
Assertions.assertEquals(1..10, sum(1..5, 6..10))
}
I/O
仍然还是使用的 java.io.File
类,不过 Groovy 在 ResourceGroovyMethods
中对 File
扩展了很多方法,使用起来更加方便。
@Test
void def_file() {
new File('C:/','example.txt').withWriter('utf-8') {
writer -> writer.writeLine 'Hello,World!'
}
new File('C:/example.txt').eachLine {
line -> println "line : $line";
}
}
字符串
和 Java 的字符串稍有区别。Groovy 中字符串定义支持以下几种方式:
'
: 单行字符串'''
: 支持换行"
: 支持插值,包含插值时类型为GString
"""
: 支持换行,支持插值,包含插值时类型为GString
插值支持两种格式:${variable}
或 $variable
。
@Test
void def_string() {
def a = 'Hello,World!'
Assertions.assertEquals('Hello,World!', a)
def name = 'JiaJia'
def b = "Hello,${name}!"
assert b instanceof GString
Assertions.assertEquals('Hello,JiaJia!', b as String)
def c = """line1
line2"""
Assertions.assertEquals(2, c.split('\n').length)
}
列表
列表的定义方式比较像 JavaScript ,可以直接在代码使用字面量定义,使用起来很方便。
Java 中只能使用类似 Arrays.asList()
的方法在定义的同时初始化值。
@Test
void def_list() {
def emptyList = []
Assertions.assertEquals(0, emptyList.size())
def numberList = [1, 3, 5, 7, 9]
Assertions.assertEquals(9, numberList.max())
}
映射
Map
的定义也和 JavaScript 比较像,只是把 {}
换成了 []
。
Java 中需要使用 Map.of()
、Collections.emptyMap()
、Collections.singletonMap()
等方法在定义的同时初始化值。
@Test
void def_map() {
def emptyMap = [:]
Assertions.assertEquals(0, emptyMap.size())
def productMap = ['0001': '商品1', '0002': '商品2']
Assertions.assertEquals('商品1', productMap.get('0001'))
}
正则表达式
在 Java 中正则表达式匹配需要借助 Pattern
类来实现。正则表达式的语法不知道有没有区别,应该都是一样的。
在 Groovy 中使用以下三个标识符执行正则匹配相关操作:
~
: 定义正则表达式,Pattern
类型;=~
: 查询操作符,返回值的类型是Matcher
,但是在if
等判断语句中返回的是Boolean
型(在判断语句中的返回值应该是);==~
: 匹配操作符,返回值是Boolean
型,只有在整个字符串完全匹配正则表达式时才会返回True
。
@Test
void def_regex() {
def ooPattern = ~'.*oo.*'
Assertions.assertTrue(ooPattern instanceof Pattern)
Assertions.assertTrue('Groovy'.matches(ooPattern))
def ooMatcher = 'Groovy' =~ 'oo'
Assertions.assertTrue(ooMatcher instanceof Matcher)
if ('Groovy' =~ 'oo') {
Assertions.assertTrue(true)
} else {
Assertions.assertTrue(false)
}
Assertions.assertTrue(('Groovy' =~ 'Groovy').any())
Assertions.assertTrue(('Groovy' =~ 'oo').any())
Assertions.assertTrue(('Groovy' =~ '^G').any())
Assertions.assertTrue(('Groovy' =~ 'y$').any())
Assertions.assertTrue(('Groovy' =~ 'Groovy').matches())
Assertions.assertFalse(('Groovy' =~ 'oo').matches())
Assertions.assertFalse(('Groovy' =~ '^G').matches())
Assertions.assertFalse(('Groovy' =~ 'y$').matches())
Assertions.assertTrue('Groovy' ==~ 'Groovy')
Assertions.assertFalse('Groovy' ==~ 'oo')
Assertions.assertTrue('Groovy' ==~ 'Gro*vy')
Assertions.assertTrue('Groovy' ==~ 'Gro{2}vy')
}
面向对象
这里以简单的 POJO 为例对比下 Java 和 Groovy 的区别。
class Student {
int id;
private String name;
}
主要是字段默认访问控制符(即不写访问控制符)的不同:
- Java:同一个包内可访问
- Groovy:可见性等同于
public
,并且会自动生成 getter 和 setter- 仅默认访问控制符会自动生成 getter 和 setter ,其他访问控制符均需要手动添加
另外所有 Groovy 对象的基类 GroovyObject
还提供了 getProperty
和 setProperty
方法。
特征 trait
这个关键字 trait
在 Java 中没有对应的功能。试了一下,个人感觉大体上可以理解成一个允许定义私有字段的 interface
。用法基本上和 interface
类似。
interface HelloInterface {
String INTERFACE_NAME = 'HELLO INTERFACE'
String hello()
// default String hello() {
// 'Hello,World!'
// }
}
interface GoodbyeInterface {
String INTERFACE_NAME = 'GOODBYE INTERFACE'
String goodbye()
// default String goodbye() {
// 'Goodbye,World!'
// }
}
trait HelloTrait implements HelloInterface {
private String name
@Override
String hello() {
"Hello,${(this.name ?: 'World')}!"
}
void setName(String name) {
this.name = name
}
void doSomething() {
}
}
trait GoodbyeTrait implements GoodbyeInterface {
private String name
@Override
String goodbye() {
"Goodbye,${(this.name ?: 'World')}!"
}
void setName(String name) {
this.name = name
}
void doSomethingElse() {
}
}
class ChatService implements HelloTrait, GoodbyeTrait {
/**
* 如果不覆写 setName 方法,则会显示警告:特征 HelloTrait, GoodbyeTrait 包含带签名 setName(String) 的冲突方法
* 此时如果直接调用 setName 方法,默认执行后实现的特征,即这里的 GoodbyeTrait
* 这里手动指定为调用 HelloTrait 的 setName 方法
*
* @param name
*/
@Override
void setName(String name) {
HelloTrait.super.setName(name)
}
}
由于 ChatService
中的 setName
指定为调用 HelloTrait
中的方法,所有下面测试代码中的 setName
调用始终修改的是 HelloTrait
中的私有字段。
@Test
void def_trait() {
// 如果将 HelloInterface / GoodbyeInterface 的方法定义为 default 方法并添加默认方法体,则这两个原本的常量将找不到,貌似自动转为了字段
Assertions.assertEquals('HELLO INTERFACE', HelloInterface.INTERFACE_NAME)
Assertions.assertEquals('GOODBYE INTERFACE', GoodbyeInterface.INTERFACE_NAME)
ChatService chatService = new ChatService()
chatService.setName('JiaJia')
Assertions.assertEquals('Hello,JiaJia!', chatService.hello())
Assertions.assertEquals('Goodbye,World!', chatService.goodbye())
((HelloTrait) chatService).setName('Momo')
Assertions.assertEquals('Hello,Momo!', chatService.hello())
Assertions.assertEquals('Goodbye,World!', chatService.goodbye())
((GoodbyeTrait) chatService).setName("Kobe")
Assertions.assertEquals('Hello,Kobe!', chatService.hello())
Assertions.assertEquals('Goodbye,World!', chatService.goodbye())
}
其他感觉还有一些微妙的区别,比如将 HelloInterface
中的方法修改为 default
方法时,貌似整个 interface
的定义都变了,感觉自动变成了 trait
。刚学习 Groovy,不知道具体是咋样的,以后看到了再补充吧。
闭包 closure
闭包是一个短的匿名代码块。Java 8 已经支持了闭包。有时候也叫做匿名方法。
下面的测试代码中展示了几种场景的定义和调用闭包的方法:
@Test
void def_closure() {
def name = 'JiaJia'
def clos = { println "Hello,${name}!" }
assert clos instanceof Closure
// print Hello,JiaJia!
clos.call()
// print Hello,JiaJia!
(() -> println "Hello,${name}!")()
// print Hello,JiaJia!
(param -> println "Hello,${param}!") name
}
DSLS
Groovy 允许在顶层语句的方法调用的参数周围省略括号。 这被称为“命令链”功能。
这个扩展的工作原理是允许链接这种无括号的方法调用,在参数周围不需要括号,也不需要链接调用之间的点。如果有多个参数的话,参数之间的逗号( ,
)还是需要的。
比如 a b c d
实际上相当于 a(b).c(d)
。
println 'Hello,World!'
教程文档里的示例相当经典,使用 DSLS 可以让你使用如下的方式创建实例。是不是很像 C# 中的 LINQ?
@Test
void def_email_dsl() {
def email = EmailDsl.make {
to 'NIU'
from 'JiaJia'
body 'How are things? I am doing well. Take care!'
}
assert email instanceof EmailDsl
println email
}
EmailDsl
类的代码如下(基于教程稍有改动):
class EmailDsl {
String toText
String fromText
String body
/**
* This method accepts a closure which is essentially the DSL. Delegate the
* closure methods to
* the DSL class so the calls can be processed
*/
def static make(closure) {
EmailDsl emailDsl = new EmailDsl()
// any method called in closure will be delegated to the EmailDsl class
closure.delegate = emailDsl
closure()
return emailDsl
}
/**
* Store the parameter as a variable and use it later to output a memo
*/
def to(String toText) {
this.toText = toText
}
def from(String fromText) {
this.fromText = fromText
}
def body(String bodyText) {
this.body = bodyText
}
@Override
String toString() {
"""FROM ${this.fromText}
TO ${this.toText}
${this.body}"""
}
}