Groovy 闭包(Closures)

本章介绍Groovy闭包。Groovy中的闭包是一个开放的、匿名的代码块,它可以接受参数、返回值并分配给变量。闭包可以引用在其周围范围中声明的变量。与闭包的形式化定义相反,Groovy语言中的闭包还可以包含在其周围范围之外定义的自由变量。在打破闭包的形式概念的同时,它提供了本章所描述的各种优势。

1. 语法

1.1. 定义闭包

闭包定义遵循以下语法:

{ [closureParameters -> ] statements }

其中,[closureParameters->]是一个可选的逗号分隔的参数列表,语句是0个或多个Groovy语句。这些参数看起来类似于方法参数列表,这些参数可以是类型化的,也可以是非类型化的。

指定参数列表时,需要->字符,用于将参数与闭包体分开。语句部分由0、1或许多Groovy语句组成。

有效闭包定义的一些示例:

{ item++ }                               // 引用名为item的变量的闭包

{ -> item++ }                            // 通过添加箭头(->)显示的区分参数与代码

{ println it }                           // 使用隐式参数it

{ it -> println it }                     // 显式使用参数it

{ name -> println name }                 // 使用显式参数name要比使用隐式参数更好一些

{ String x, int y ->                     // 接收两种类型参数的闭包
    println "hey ${x} the value is ${y}"
}

{ reader ->                              // 包含多个语句的闭包
    def line = reader.readLine()
    line.trim()
}

1.2. 闭包作为对象

闭包是groovy.lang.closure类的一个实例,使得它可以像任何其他变量一样分配给变量或字段,尽管它是一个代码块:

def listener = { e -> println "Clicked on $e.source" }
assert listener instanceof Closure
Closure callback = { println 'Done!' }
Closure<Boolean> isTextFile = {
    File it -> it.name.endsWith('.txt')
}

You can assign a closure to a variable, and it is an instance of groovy.lang.Closure

If not using def or var, use groovy.lang.Closure as the type

Optionally, you can specify the return type of the closure by using the generic type of groovy.lang.Closure

1.3. 调用闭包

闭包作为一个匿名代码块,可以像调用任何其他方法一样进行调用。如果定义的闭包不接受这样的参数:

def code = { 123 }

那么闭包内的代码只有在调用闭包时才会执行,这可以通过使用变量来完成,就像它是一个常规方法一样:

assert code() == 123

或者,可以显式地使用call方法:

assert code.call() == 123

如果闭包接受参数,则原理相同:

def isOdd = { int i -> i%2 != 0 }
assert isOdd(3) == true
assert isOdd.call(2) == false

def isEven = { it%2 == 0 }
assert isEven(3) == false
assert isEven.call(2) == true

define a closure which accepts an int as a parameter

it can be called directly

or using the call method

same goes for a closure with an implicit argument (it)

which can be called directly using (arg)

or using call

与方法不同,闭包在被调用时总是返回一个值。下一节将讨论如何声明闭包参数、何时使用它们以及隐式的“it”参数是什么。

2. 参数

2.1. 普通参数

闭包的参数遵循与常规方法参数相同的原则:

  • 可选类型

  • 一个名字

  • 可选的默认值

  • 参数用逗号分隔:

def closureWithOneArg = { str -> str.toUpperCase() }
assert closureWithOneArg('groovy') == 'GROOVY'

def closureWithOneArgAndExplicitType = { String str -> str.toUpperCase() }
assert closureWithOneArgAndExplicitType('groovy') == 'GROOVY'

def closureWithTwoArgs = { a,b -> a+b }
assert closureWithTwoArgs(1,2) == 3

def closureWithTwoArgsAndExplicitTypes = { int a, int b -> a+b }
assert closureWithTwoArgsAndExplicitTypes(1,2) == 3

def closureWithTwoArgsAndOptionalTypes = { a, int b -> a+b }
assert closureWithTwoArgsAndOptionalTypes(1,2) == 3

def closureWithTwoArgAndDefaultValue = { int a, int b=2 -> a+b }
assert closureWithTwoArgAndDefaultValue(1) == 3

2.2. 隐式参数

当闭包没有显式定义参数列表(使用->)时,闭包总是定义一个名为它的隐式参数。这意味着该代码:

def greeting = { "Hello, $it!" } 
assert greeting('Patrick') == 'Hello, Patrick!'

等价于:

def greeting = { it -> "Hello, $it!" } 
assert greeting('Patrick') == 'Hello, Patrick!'

如果要声明不接受参数且必须限制为不带参数的调用的闭包,则必须使用显式空参数列表声明它:

def magicNumber = { -> 42 } 
// this call will fail because the closure doesn't accept any argument 
magicNumber(11)

2.3. 可变参数

闭包可以像其他方法一样声明变量参数。可变参数方法是一种方法,如果最后一个参数的长度(或数组)可变,则可以接受数量可变的参数,如下例所示:

def concat1 = { String... args -> args.join('') }
assert concat1('abc','def') == 'abcdef'
def concat2 = { String[] args -> args.join('') }
assert concat2('abc', 'def') == 'abcdef'

def multiConcat = { int n, String... args ->
    args.join('')*n
}
assert multiConcat(2, 'abc','def') == 'abcdefabcdef'

A closure accepting a variable number of strings as first parameter

It may be called using any number of arguments without having to explicitly wrap them into an array

The same behavior is directly available if the args parameter is declared as an array

As long as the last parameter is an array or an explicit vargs type

3. 授权策略

3.1. Groovy闭包与lambda表达式

Groovy将闭包定义为闭包类的实例。它与Java8中的lambda表达式有很大的不同。委托是Groovy闭包中的一个关键概念,在lambdas中没有等价的概念。更改委托或更改闭包的委托策略的能力使得在Groovy中设计漂亮的领域特定语言(dsl)成为可能。

3.2. Owner, delegate 和 this

为了理解委托的概念,我们必须首先在闭包中解释委托的含义。闭包实际上定义了3个不同的东西:

  • this对应于定义闭包的封闭类

  • owner对应于定义闭包的封闭对象,它可以是类或闭包

  • delegate对应于第三方对象,在该对象中,每当未定义消息的接收者时,就解析方法调用或属性

3.2.1. this的意义

在闭包中,调用getThisObject将返回定义闭包的封闭类。这相当于使用显式表达式:

class Enclosing {
    void run() {
        def whatIsThisObject = { getThisObject() }  // 在Enclosing类中定义了一个闭包,并返回了getThisObject
        assert whatIsThisObject() == this           // 调用这个闭包,会返回Enclosing的实例
        def whatIsThis = { this }                   // getThisObject方法返回的就是this,两者是等价的
        assert whatIsThis() == this                 // 它返回了完全相同的对象
    }
}
class EnclosedInInnerClass {
    class Inner {
        Closure cl = { this }                       // 在内部类中定义了一个闭包
    }
    void run() {
        def inner = new Inner()
        assert inner.cl() == inner                  // 返回的this指向的是内部类的实例,不是外面的类
    }
}
class NestedClosures {
    void run() {
        def nestedClosures = {
            def cl = { this }                       // 嵌套的闭包中,cl定义在了nestedClosures闭包中
            cl()
        }
        assert nestedClosures() == this             // 结果nestedClosures()的结果指向的是最靠近的外部类,而不是最近的闭包
    }
}

当然,可以通过以下方式从封闭类调用方法:

class Person {
    String name
    int age
    String toString() {
       "$name is $age years old" 
    }

    String dump() {
        def cl = {
            String msg = this.toString()  //闭包对此调用toString,这实际上会调用封闭对象(即Person实例)上的toString方法
            println msg
            msg
        }
        cl()
    }
}
def p = new Person(name:'Janice', age:74)
assert p.dump() == 'Janice is 74 years old'

3.2.2. 闭包的所有者

闭包的所有者与闭包中的定义非常相似,但有一个细微的区别:它将返回直接封闭的对象,无论是闭包还是类:

class Enclosing {
    void run() {
        def whatIsOwnerMethod = { getOwner() }  // 在Enclosing类中定义了一个闭包,并返回getOwner
        assert whatIsOwnerMethod() == this      // 调用闭包后会返回Enclosing的实例
        def whatIsOwner = { owner }             // owner与getOwner等价,owner是getOwner的简写
        assert whatIsOwner() == this            // 两者是完全相同的对象
    }
}
class EnclosedInInnerClass {
    class Inner {
        Closure cl = { owner }                  // 如果闭包定义在内部类中
    }
    void run() {
        def inner = new Inner()
        assert inner.cl() == inner              // 闭包中的owner会返回内部类的实例,而不是外部类的。
    }
}
class NestedClosures {
    void run() {
        def nestedClosures = {
            def cl = { owner }                  // 在嵌套闭包的情况下,cl就定义在了闭包nestedClosures内。
            cl()
        }
        assert nestedClosures() == nestedClosures   // owner返回的结果是最近的闭包,因此和对象的this不同。
    }
}

3.2.3. 闭包的委托

可以通过使用delegate属性或调用getDelegate方法来访问闭包的委托。它是在Groovy中构建领域特定语言的强大概念。当闭包和闭包所有者引用闭包的词法范围时,委托是闭包将使用的用户定义对象。默认情况下,委托设置为所有者:

class Enclosing {
    void run() {
        def cl = { getDelegate() }  // 通过调用getDelegate方法来获得一个闭包的委托
        def cl2 = { delegate }      // delegate与getDelegate等价
        assert cl() == cl2()        // 两个返回相同的对象
        assert cl() == this         // 返回外部类或闭包
        def enclosed = {
            { -> delegate }.call()  // 特殊的嵌套闭包
        }
        assert enclosed() == enclosed  // delegate返回的结果和owner一样
    }
}

you can get the delegate of a closure calling the getDelegate method

or using the delegate property

both return the same object

which is the enclosing class or closure

in particular in case of nested closures

delegate will correspond to the owner

闭包的委托可以更改为任何对象。让我们通过创建两个类来说明这一点,它们不是彼此的子类,但都定义了一个名为name的属性:

class Person {
    String name
}
class Thing {
    String name
}

def p = new Person(name: 'Norman')
def t = new Thing(name: 'Teapot')

然后,让我们定义一个闭包来获取委托的name属性:

def upperCasedName = { delegate.name.toUpperCase() }

然后通过更改闭包的委托,可以看到目标对象将发生更改:

upperCasedName.delegate = p 
assert upperCasedName() == 'NORMAN' 
upperCasedName.delegate = t 
assert upperCasedName() == 'TEAPOT'

此时,行为与在闭包的词汇范围中定义目标变量没有区别:

def target = p 
def upperCasedNameUsingVar = { target.name.toUpperCase() } 
assert upperCasedNameUsingVar() == 'NORMAN'

但是,有一些主要区别:

  • 在上一个示例中,target是从闭包中引用的局部变量

  • 委托可以透明地使用,也就是说,不必在方法调用前面加上delegate。如下一段所述。

3.2.4. 授权策略

无论何时,在闭包中,在没有显式设置接收方对象的情况下访问属性,都会涉及委派策略:

class Person {
    String name
}
def p = new Person(name:'Igor')
def cl = { name.toUpperCase() }  // 在闭包中name属性并没有定义,直接调用cl会报异常。
cl.delegate = p                  // 这个时候改变cl闭包的委托,指向Person的实例
assert cl() == 'IGOR'            // 方法成功运行

此代码工作的原因是name属性将在委托对象上透明地解析!这是解析闭包内的属性或方法调用的一种非常强大的方法。不需要设置显式委托。接收者:将进行调用,因为闭包的默认委派策略使其如此。闭包实际上定义了多种可供选择的解决策略:

  • Closure.OWNER_FIRST is the default strategy. If a property/method exists on the owner, then it will be called on the owner. If not, then the delegate is used.

  • Closure.DELEGATE_FIRST reverses the logic: the delegate is used first, then the owner

  • Closure.OWNER_ONLY will only resolve the property/method lookup on the owner: the delegate will be ignored.

  • Closure.DELEGATE_ONLY will only resolve the property/method lookup on the delegate: the owner will be ignored.

  • Closure.TO_SELF can be used by developers who need advanced meta-programming techniques and wish to implement a custom resolution strategy: the resolution will not be made on the owner or the delegate but only on the closure class itself. It makes only sense to use this if you implement your own subclass of Closure.

让我们用以下代码来说明默认的“所有者优先”策略:

class Person {
    String name
    def pretty = { "My name is $name" }
    String toString() {
        pretty()
    }
}
class Thing {
    String name
}

def p = new Person(name: 'Sarah')
def t = new Thing(name: 'Teapot')

assert p.toString() == 'My name is Sarah'
p.pretty.delegate = t
assert p.toString() == 'My name is Sarah'

for the illustration, we define a closure member which references "name"

both the Person and the Thing class define a name property

Using the default strategy, the name property is resolved on the owner first

so if we change the delegate to t which is an instance of Thing

there is no change in the result: name is first resolved on the owner of the closure

However, it is possible to change the resolution strategy of the closure:

p.pretty.resolveStrategy = Closure.DELEGATE_FIRST 
assert p.toString() == 'My name is Teapot'

通过更改resolveStrategy,我们修改了Groovy解析“隐式this”引用的方式:在本例中,名称将首先在委托中查找,如果找不到,则在所有者中查找。因为名称是在委托(Thing的实例)中定义的,所以使用这个值。

The difference between "delegate first" and "delegate only" or "owner first" and "owner only" can be illustrated if one of the delegate (resp. owner) does not have such a method or property:

class Person {
    String name
    int age
    def fetchAge = { age }
}
class Thing {
    String name
}

def p = new Person(name:'Jessica', age:42)
def t = new Thing(name:'Printer')
def cl = p.fetchAge
cl.delegate = p
assert cl() == 42
cl.delegate = t
assert cl() == 42
cl.resolveStrategy = Closure.DELEGATE_ONLY
cl.delegate = p
assert cl() == 42
cl.delegate = t
try {
    cl()
    assert false
} catch (MissingPropertyException ex) {
    // "age" is not defined on the delegate
}

In this example, we define two classes which both have a name property but only the Person class declares an age. The Person class also declares a closure which references age. We can change the default resolution strategy from "owner first" to "delegate only". Since the owner of the closure is the Person class, then we can check that if the delegate is an instance of Person, calling the closure is successful, but if we call it with a delegate being an instance of Thing, it fails with a groovy.lang.MissingPropertyException. Despite the closure being defined inside the Person class, the owner is not used.


A comprehensive explanation about how to use this feature to develop DSLs can be found in a dedicated section of the manual.

4. Closures in GStrings

Take the following code:

def x = 1 
def gs = "x = ${x}" 
assert gs == 'x = 1'

The code behaves as you would expect, but what happens if you add:

x = 2
assert gs == 'x = 2'

You will see that the assert fails! There are two reasons for this:

  • a GString only evaluates lazily the toString representation of values

  • the syntax ${x} in a GString does not represent a closure but an expression to $x, evaluated when the GString is created.

In our example, the GString is created with an expression referencing x. When the GString is created, the value of x is 1, so the GString is created with a value of 1. When the assert is triggered, the GString is evaluated and 1 is converted to a String using toString. When we change x to 2, we did change the value of x, but it is a different object, and the GString still references the old one.


A GString will only change its toString representation if the values it references are mutating. If the references change, nothing will happen.

If you need a real closure in a GString and for example enforce lazy evaluation of variables, you need to use the alternate syntax ${→ x} like in the fixed example:

def x = 1 
def gs = "x = ${-> x}" 
assert gs == 'x = 1' x = 2 
assert gs == 'x = 2'

And let’s illustrate how it differs from mutation with this code:

class Person {
    String name
    String toString() { name }
}
def sam = new Person(name:'Sam')
def lucy = new Person(name:'Lucy')
def p = sam
def gs = "Name: ${p}"
assert gs == 'Name: Sam'
p = lucy
assert gs == 'Name: Sam'
sam.name = 'Lucy'
assert gs == 'Name: Lucy'

the Person class has a toString method returning the name property

we create a first Person named Sam

we create another Person named Lucy

the p variable is set to Sam

and a closure is created, referencing the value of p, that is to say Sam

so when we evaluate the string, it returns Sam

if we change p to Lucy

the string still evaluates to Sam because it was the value of p when the GString was created

so if we mutate Sam to change the name to Lucy

this time the GString is correctly mutated

So if you don’t want to rely on mutating objects or wrapping objects, you must use closures in GString by explicitly declaring an empty argument list:

class Person {
    String name
    String toString() { name }
}
def sam = new Person(name:'Sam')
def lucy = new Person(name:'Lucy')
def p = sam
// Create a GString with lazy evaluation of "p"
def gs = "Name: ${-> p}"
assert gs == 'Name: Sam'
p = lucy
assert gs == 'Name: Lucy'

5. Closure coercion

Closures can be converted into interfaces or single-abstract method types. Please refer to this section of the manual for a complete description.

6. Functional programming

Closures, like lambda expressions in Java 8 are at the core of the functional programming paradigm in Groovy. Some functional programming operations on functions are available directly on the Closure class, like illustrated in this section.

6.1. Currying

In Groovy, currying refers to the concept of partial application. It does not correspond to the real concept of currying in functional programming because of the different scoping rules that Groovy applies on closures. Currying in Groovy will let you set the value of one parameter of a closure, and it will return a new closure accepting one less argument.

6.1.1. Left currying

Left currying is the fact of setting the left-most parameter of a closure, like in this example:

def nCopies = { int n, String str -> str*n }  
def twice = nCopies.curry(2)  
assert twice('bla') == 'blabla'  
assert twice('bla') == nCopies(2, 'bla')

the nCopies closure defines two parameters

curry will set the first parameter to 2, creating a new closure (function) which accepts a single String

so the new function call be called with only a String

and it is equivalent to calling nCopies with two parameters

6.1.2. Right currying

Similarily to left currying, it is possible to set the right-most parameter of a closure:

def nCopies = { int n, String str -> str*n }  
def blah = nCopies.rcurry('bla')  
assert blah(2) == 'blabla'  
assert blah(2) == nCopies(2, 'bla')

the nCopies closure defines two parameters

rcurry will set the last parameter to bla, creating a new closure (function) which accepts a single int

so the new function call be called with only an int

and it is equivalent to calling nCopies with two parameters

6.1.3. Index based currying

In case a closure accepts more than 2 parameters, it is possible to set an arbitrary parameter using ncurry:

def volume = { double l, double w, double h -> l*w*h }  
def fixedWidthVolume = volume.ncurry(1, 2d)  
assert volume(3d, 2d, 4d) == fixedWidthVolume(3d, 4d)  
def fixedWidthAndHeight = volume.ncurry(1, 2d, 4d)  
assert volume(3d, 2d, 4d) == fixedWidthAndHeight(3d)

the volume function defines 3 parameters

ncurry will set the second parameter (index = 1) to 2d, creating a new volume function which accepts length and height

that function is equivalent to calling volume omitting the width

it is also possible to set multiple parameters, starting from the specified index

the resulting function accepts as many parameters as the initial one minus the number of parameters set by ncurry

6.2. Memoization

Memoization allows the result of the call of a closure to be cached. It is interesting if the computation done by a function (closure) is slow, but you know that this function is going to be called often with the same arguments. A typical example is the Fibonacci suite. A naive implementation may look like this:

def fib 
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) } 
assert fib(15) == 610 // slow!

It is a naive implementation because 'fib' is often called recursively with the same arguments, leading to an exponential algorithm:

  • computing fib(15) requires the result of fib(14) and fib(13)

  • computing fib(14) requires the result of fib(13) and fib(12)

Since calls are recursive, you can already see that we will compute the same values again and again, although they could be cached. This naive implementation can be "fixed" by caching the result of calls using memoize:

fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }.memoize() 
assert fib(25) == 75025 // fast!

The cache works using the actual values of the arguments. This means that you should be very careful if you use memoization with something else than primitive or boxed primitive types.

The behavior of the cache can be tweaked using alternate methods:

  • memoizeAtMost will generate a new closure which caches at most n values

  • memoizeAtLeast will generate a new closure which caches at least n values

  • memoizeBetween will generate a new closure which caches at least n values and at most n values

The cache used in all memoize variants is a LRU cache.

6.3. Composition

Closure composition corresponds to the concept of function composition, that is to say creating a new function by composing two or more functions (chaining calls), as illustrated in this example:

def plus2  = { it + 2 }
def times3 = { it * 3 }

def times3plus2 = plus2 << times3
assert times3plus2(3) == 11
assert times3plus2(4) == plus2(times3(4))

def plus2times3 = times3 << plus2
assert plus2times3(3) == 15
assert plus2times3(5) == times3(plus2(5))

// reverse composition
assert times3plus2(3) == (times3 >> plus2)(3)

6.4. Trampoline

Recursive algorithms are often restricted by a physical limit: the maximum stack height. For example, if you call a method that recursively calls itself too deep, you will eventually receive a StackOverflowException.

An approach that helps in those situations is by using Closure and its trampoline capability.

Closures are wrapped in a TrampolineClosure. Upon calling, a trampolined Closure will call the original Closure waiting for its result. If the outcome of the call is another instance of a TrampolineClosure, created perhaps as a result to a call to the trampoline() method, the Closure will again be invoked. This repetitive invocation of returned trampolined Closures instances will continue until a value other than a trampolined Closure is returned. That value will become the final result of the trampoline. That way, calls are made serially, rather than filling the stack.

Here’s an example of the use of trampoline() to implement the factorial function:

def factorial
factorial = { int n, def accu = 1G ->
    if (n < 2) return accu
    factorial.trampoline(n - 1, n * accu)
}
factorial = factorial.trampoline()

assert factorial(1)    == 1
assert factorial(3)    == 1 * 2 * 3
assert factorial(1000) // == 402387260.. plus another 2560 digits

6.5. 方法指针

通常可以使用常规方法作为闭包。例如,您可能希望使用闭包的currying功能,但这些功能对普通方法不可用。在Groovy中,可以使用method-pointer操作符从任何方法获得闭包。