Skip to content

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

groovy
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 创建标识符。标识符被用来定义变量、函数或其他用户定义的变量。标识符以字母,美元或下划线开头。定义变量、函数时仍然可以使用具体的类名。

groovy
@Test
void def_variable() {
    def x = 5
    Assertions.assertEquals(5, x)
}

范围运算符

Groovy 支持范围的概念,并在 .. 符号的帮助下提供范围运算符的符号。范围是指定值序列的速记。

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' - 范围也可以由字符组成并按降序排列。
groovy
@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 关键字定义的。
  • 方法可以接收任意数量的参数。
  • 定义参数时,不必显式定义类型。
  • 可以添加修饰符,如 publicprivateprotected
  • 默认情况下,如果未提供可见性修饰符,则该方法为 public
  • 有返回值的方法 return 可以被省略,默认返回最后一行代码的运行结果。
groovy
void sayHello() {
    println('Hello,World!')
}

方法参数

groovy
static def sum(a, b) {
    a + b
}

默认参数

groovy
def someMethod(parameter1, parameter2 = 0, parameter3 = 0) {
   // Method code goes here
}

“可选”类型

Groovy 是一种“可选”类型的语言,而 Java 是一种“强”类型的语言。

def 类型的变量或方法返回值,会在运行时根据分配的值来确定其类型。和 Java 中的 var 还是有很大区别的,var 只是一种简写,编译时仍然会推导变量的具体类型。

下面定义的变量 xInteger 型:

groovy
def x = 5
assert x instanceof Integer

但是之后仍然可以让其赋值为其他类型的值:

groovy
@Test
void def_variable() {
    def x = 5
    assert x instanceof Integer
    assert 5 == x
    x = 1..5
    assert x instanceof IntRange
}

下面 sum 方法的写法在 Java 中就只能用泛型来实现,相比较来说 Groovy 中的写法简单很多。

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 扩展了很多方法,使用起来更加方便。

groovy
@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 中字符串定义支持以下几种方式:

  1. ' : 单行字符串
  2. ''' : 支持换行
  3. " : 支持插值,包含插值时类型为 GString
  4. """ : 支持换行,支持插值,包含插值时类型为 GString

插值支持两种格式:${variable}$variable

groovy
@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() 的方法在定义的同时初始化值。

groovy
@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() 等方法在定义的同时初始化值。

groovy
@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
groovy
@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 的区别。

groovy
class Student {
    int id;
    private String name;
}

主要是字段默认访问控制符(即不写访问控制符)的不同:

  • Java:同一个包内可访问
  • Groovy:可见性等同于 public ,并且会自动生成 gettersetter
    • 仅默认访问控制符会自动生成 gettersetter ,其他访问控制符均需要手动添加

另外所有 Groovy 对象的基类 GroovyObject 还提供了 getPropertysetProperty 方法。

特征 trait

这个关键字 trait 在 Java 中没有对应的功能。试了一下,个人感觉大体上可以理解成一个允许定义私有字段的 interface 。用法基本上和 interface 类似。

groovy
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 中的私有字段。

groovy
@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 已经支持了闭包。有时候也叫做匿名方法。

下面的测试代码中展示了几种场景的定义和调用闭包的方法:

groovy
@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)

groovy
println 'Hello,World!'

教程文档里的示例相当经典,使用 DSLS 可以让你使用如下的方式创建实例。是不是很像 C# 中的 LINQ?

groovy
@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 类的代码如下(基于教程稍有改动):

groovy
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}"""
    }
}