Most of the following content comes from the Groovy official documentation. If you are OK with English, you are advised to check the official documentation directly. Related Groovy examples have been uploaded to ZJXStar’s GitHub.
preface
In Java, it is common to define method parameters to register event handlers by creating anonymous inner classes, but this can make the code very verbose. Closures in Groovy remove this verbosity. Closures are lightweight, short, and compact, and are one of Groovy’s most important and powerful features.
Groovy’s Closure is an open, anonymous block of code. Closures are also objects that can pass arguments, return values and assign them to variables. Closures in Groovy even break the formal notion that closures can contain free variables outside their scope.
Benefits of closures
Closures in Groovy eliminate verbosity and aid in creating lightweight, reusable snippets of code. Here’s a simple example to give you a taste of the convenience of closures.
Example: Find the sum of all odd numbers between 1 and n.
In a traditional implementation, we define a sum method that uses the for loop to add up odd numbers as follows:
def sum(n) {
total = 0
for (int i = 1; i <= n; i += 2) {
// Calculate the odd sum from 1 to n
total += i
}
total
}
println(sum(9)) // n equals 9
Copy the code
What if I take the product of all odd numbers instead? Then define a multiply method, again using a for loop, as follows:
def multiply(n) {
total = 1
for (int i = 1; i <= n; i += 2) {
// Compute the odd number product from 1 to n
total *= i
}
total
}
Copy the code
What if I take the sum of the squares of an odd number? The implementation code is basically the same as sum and multiply, repeated for loop, different calculation method.
Three methods are defined in just three cases, and there is a lot of repetitive code, which is just too inelegant. What about using closures? We simply define a higher-order function and pass in a Closure argument.
def pickOdd(n, closure) { // Closure can be replaced with a different argument name
for (int i = 1; i <= n; i += 2) {
// The execution logic is determined by the closure passed in
closure(i)
}
}
Copy the code
Groovy closures can be attached to a method or assigned to a variable. In the example, the closure variable maintains a reference to the closure. When you use the pickOdd method, you simply pass in the closure method block that contains the different logic.
// Print odd numbers
pickOdd(9) {
println it More on it below
}
/ / sum
total = 0
pickOdd(9, {
num -> total += num // You can access the total variable
})
println("Sum of odd is: ${total}")
Copy the code
Obviously, implementation through closures is not only syntactically more elegant than the traditional approach, but also provides a simple and convenient way for functions to delegate part of their implementation logic.
Define and use closures
Two closures were simply implemented in the previous section. Intuitively, a closure is a block of code wrapped in braces. This section details the syntax for defining closures in Groovy and how they are used.
Define the closure
The syntax for defining closures is simple (somewhat similar to Lambda expressions in Java 8) :
{ [closureParameters -> ] statements }
Copy the code
In the syntax, [closureParameters ->] represents a list of parameters, which can be zero or more, and it is optional to declare the parameter type. If an argument list is declared, then -> is required to distinguish arguments from execution statements. Statements represent execution statements and generally have at least one piece of execution logic (or none). Of course, you could write an empty closure {}, but that doesn’t make much sense.
Here are some examples of valid closures:
{} // Receive an argument, empty execution statement
{ ++it } // Accept an argument, default it
{ -> println('no param')} // No arguments, must write ->
// You can declare parameter types
{ String x, int y -> println("x is ${x}, y is ${y}")}// You can omit the parameter type
{ x, y -> println("x is ${x}, y is ${y}")}// One argument, multiple lines of execution statement
{ x ->
int y = -x
println("-x is ${y}")}Copy the code
As mentioned earlier, a Closure is also an object; yes, it is an instance of the Groovy.lang.Closure class. So we can assign it to a variable.
def closure4 = { x, y -> println("x is ${x}, y is ${y}")}// omit the type
Closure closure6 = { println('Done')}// Declaration type Closure
Closure<Boolean> closure7 = { int a, int b -> a == b } // Return types can be defined
Copy the code
Using closures
Closures are very simple to use; we can call a closure either as a method or explicitly through the Call method.
def isOdd = { int i -> i%2! =0 }
assert isOdd(3) = =true // Similar to method calls
assert isOdd.call(2) = =false // Use call method
Copy the code
Closures are somewhat similar to methods in usage, but there are differences. Closures always return a value when executed, which is null by default.
Closure closure6 = { println('Done')}assert closure6() == null / / returns null
def code = { 123 } // explicitly returns 123
assert code() == 123
Copy the code
Pass parameters to the closure
You can set parameters when you define a closure, and you can pass the parameters when you call a normal method. There are three types of parameters in closures: generic, hidden, and variable-length parameters, which are described in detail below.
General parameters
The rules for setting general parameters are the same as those for Groovy methods:
-
Parameter types are optional;
-
Must have parameter names;
-
Parameters can have default values, that is, default parameters;
-
Multiple parameters are separated by commas.
For example:
// A parameter without declaring its type
def closureWithOneArg = { str -> str.toUpperCase() }
assert closureWithOneArg('groovy') = ='GROOVY'
// A parameter that declares the parameter type
def closureWithOneArgAndExplicitType = { String str -> str.toUpperCase() }
assert closureWithOneArgAndExplicitType('groovy') = ='GROOVY'
// Multiple arguments, separated by commas
def closureWithTwoArgs = { a,b -> a+b }
assert closureWithTwoArgs(1.2) = =3
// Multiple parameters are declared as parameter types
def closureWithTwoArgsAndExplicitTypes = { int a, int b -> a+b }
assert closureWithTwoArgsAndExplicitTypes(1.2) = =3
// Multiple parameters, some declare parameter types
def closureWithTwoArgsAndOptionalTypes = { a, int b -> a+b }
assert closureWithTwoArgsAndOptionalTypes(1.2) = =3
// Multiple parameters, with default parameters
def closureWithTwoArgAndDefaultValue = { int a, int b=2 -> a+b }
assert closureWithTwoArgAndDefaultValue(1) = =3
Copy the code
Hidden parameters
You’ve often seen the IT keyword in the previous example, and yes, it’s a hidden parameter provided by the Groovy closure. When a closure does not display a list of definition arguments (hidden with the -> symbol), the closure provides a default parameter named it that can be used in execution statements. At this point, the closure accepts the passing of a parameter.
// Greeting1 and greeting2 are equivalent
def greeting1 = { "Hello, $it!" }
assert greeting1('Groovy') = ='Hello, Groovy! '
def greeting2 = { it -> "Hello, $it!" }
assert greeting2('Groovy') = ='Hello, Groovy! '
Copy the code
If you want the closure to accept no arguments, you must explicitly declare the -> symbol.
def magicNumber = { -> 42 }
println(magicNumber())
//magicNumber(11) // Error, no arguments accepted
Copy the code
Variable-length argument
We’ve used varied-length arguments in both Java and Groovy methods, simply by adding… Variable length parameters are denoted by symbols. The same is true for Groovy closures, so let’s look at the examples.
def concat1 = { String... args -> args.join(' ')}assert concat1('abc'.'def') = ='abcdef'
// The effect is the same as the array argument
def concat2 = { String[] args -> args.join(' ')}assert concat2('abc'.'def') = ='abcdef'
Variable-length arguments must be placed at the end of the argument list
def multiConcat = { int n, String... args ->
args.join(' ')*n
}
assert multiConcat(2.'abc'.'def') = ='abcdefabcdef'
Copy the code
Delegate strategy for closures
The biggest difference with closures, although they evolved from Lambda expressions, is the concept of delegates. The ability to modify delegate objects and delegate policies in closures is what makes them stand out in the Domain Specific Languages (DSLs) space.
To fully understand a closure’s delegate functionality, you have to learn about its three properties: this, Owner, and delegate.
this
This corresponds to the enclosing class in which the closure is defined.
In a closure, you can get the closed class that defines the closure using the getThisObject method, which is equivalent to explicitly calling this.
/ / ordinary class
class Enclosing {
void run() {
// getThisObject points to the Enclosing
def whatIsThisObject = { getThisObject() }
assert whatIsThisObject() == this
println('getThisObject(): ' + whatIsThisObject())
// This points to Enclosing
// This and getThisObject are equivalent
def whatIsThis = { this }
assert whatIsThis() == this
println('this in Closure: ' + whatIsThis())
println('this: ' + this)}}/ / inner classes
class EnclosedInInnerClass {
class Inner {
EnclosedInInnerClass EnclosedInInnerClass
Closure cl = { this}}void run() {
def inner = new Inner()
assert inner.cl() == inner
println('Closure in Inner, this: ' + inner.cl())
}
}
// A nested closure
class NestedClosures {
// This always points to NestedClosures
void run() {
def nestedClosures = {
def cl = { this }
println("cl this: " + cl())
cl()
}
assert nestedClosures() == this
println("nestedClosures this: " + nestedClosures())
println("this: " + nestedClosures())
}
}
Copy the code
As you can see from the example, this always refers to the closed class that defines the closure, which is an instance of the closed class.
owner
Owner corresponds to the enclosing object that defines the closure, which can be a class or a closure. This is similar to this, except that owner points to an object that wraps its closure directly. There is one more possibility than this: the closure object.
/ / ordinary class
class EnclosingOwner {
void run() {
// getOwner points to EnclosingOwner
def whatIsOwnerMethod = { getOwner() }
assert whatIsOwnerMethod() == this
// owner points to EnclosingOwner
def whatIsOwner = { owner }
assert whatIsOwner() == this
println('getOwner: ' + whatIsOwnerMethod())
println('owner: ' + whatIsOwner())
println('this: ' + this)}}/ / inner classes
class EnclosedInInnerClassOwner {
class Inner {
/ / points to Inner
Closure cl = { owner }
}
void run() {
def inner = new Inner()
assert inner.cl() == inner
println('inner owner: ' + inner.cl())
}
}
// A nested closure
class NestedClosuresOwner {
void run() {
def nestedClosures = {
// Refer to a nestedclosure object instead of a NestedClosuresOwner
// This is different from this
def cl = { owner }
println('Closure owner: ' + cl())
println('Closure this: ' + this)
cl()
}
assert nestedClosures() == nestedClosures
}
}
Copy the code
delegate
A delegate can correspond to any object. Both this and owner fall within the lexical scope of the closure, while delegate refers to the user-defined object used by the closure. By default, the delegate is set to owner. Delegate objects can be used using the delegate keyword and the getDelegate method.
class EnclosingDelegate {
void run() {
EnclosingDelegate (this, owner
def cl = { getDelegate() }
def cl2 = { delegate }
assert cl() == cl2()
assert cl() == this
println(cl())
// Enclosed is enclosed with the owner
def enclosed = {
{ -> delegate }.call()
}
assert enclosed() == enclosed
println(enclosed())
}
}
Copy the code
The delegate can be modified to point to any object.
class Person {
String name
}
class Thing {
String name
}
def p = new Person(name: 'Tom')
def t = new Thing(name: 'Teapot')
def upperCasedName = { delegate.name.toUpperCase() }
// upperCasedName() // An error is reported because the delegate is pointing to an object with no name property
upperCasedName.delegate = p
println(upperCasedName()) // Delegate points to Person
upperCasedName.delegate = t
println(upperCasedName()) // delegate points to Thing
Copy the code
Strategy adjustment
Closure delegates are transparent, even if we don’t explicitly use a delegate. It also works. Such as:
//name = 'jerry' // If a name is defined, the following closure prints Jerry
// Do not explicitly use delegate.
def upperCasedName2 = {
println(name.toUpperCase()) / / print to TOM
// Return owner object
owner
}
// Assign delegate directly
upperCasedName2.delegate = p
// It works fine
println(upperCasedName2()) // Prints the owner object pointing to the Groovy script
println(upperCasedName2() instanceof GroovyObject) // It's actually a GroovyObject class instance
Copy the code
The name property is properly parsed and executed thanks to the Delegate object, because the closure uses the Owner object in preference to look for the Name property and finds that it does not exist. And then we use the delegate object, which points to P and has the name property, so the method gets executed.
In fact, there are multiple delegate policies for closures:
-
Closure.OWNER_FIRST: Default policy. If the owner object has a desired property or method, it takes precedence. Otherwise, a delegate object is used. In the example above, if a name property is declared outside the closure (in the script), then the name executed in the closure is that property, not the object to which the delegate points.
-
Closure.DELEGATE_FIRST: Delegate priority policy, which, contrary to the default policy logic, gives delegate priority over owner.
-
Closure.OWNER_ONLY: Use only the owner policy, ignoring the delegate.
-
Closure.DELEGATE_ONLY: Only delegate policy is used, owner is ignored.
-
Closure.TO_SELF: Available to developers who need advanced metaprogramming techniques and want to implement custom parsing strategies. The solution will not occur on the Owner or Delegate, but only on the enclosing class itself. This strategy only makes sense if you implement your own Closure subclass.
So how do you adjust your delegation strategy? Simply assign the closure’s resolveStrategy attribute.
class Person2 {
String name
int age
def fetchAge = { age }
}
class Thing2 {
String name
}
def p2 = new Person2(name:'Jessica'.age:42)
def t2 = new Thing2(name:'Printer')
def cl = p2.fetchAge
cl.delegate = p2
assert cl() == 42 // Owner has age
cl.delegate = t2
assert cl() == 42 // The default policy is owner first
cl.resolveStrategy = Closure.DELEGATE_ONLY // Switch to using delegate only
cl.delegate = p2
assert cl() == 42 // Delegate points to p2 with an age attribute
cl.delegate = t2
try {
cl() // Delegate points to T2, but t2 has no age property and throws an exception
assert false
} catch (MissingPropertyException ex) {
// "age" is not defined on the delegate
}
Copy the code
Dynamic closures
A dynamic Closure is a code logic that determines whether the Closure passed in meets certain requirements, such as whether the Closure exists, and whether the number and type of Closure parameters meet the requirements, thus increasing the flexibility of the code.
For example:
Determines whether the method is executed with closure arguments passed in. If passed, the closure logic is executed; Otherwise, the default logic is executed.
def say(closure) {
if (closure) { // Determine if a closure was passed in
closure()
} else {
println('say nothing')
}
}
say()
say() {
println('i\'m hungry')}Copy the code
Determines whether the number of arguments passed to the closure is 2.
def compute(amount, Closure computer) {
total = 0
// Check whether the closure has 2 arguments
if (computer.maximumNumberOfParameters == 2) {
total = computer(amount, 6)}else {
total = computer(amount)
}
println("total is $total")
}
compute(100) {
it * 8 / / 800
}
compute(100) { / / 600
amount, weight ->
amount * weight
}
Copy the code
We can also specify the type of arguments to the closure.
def execute(Closure closure) {
println("params count: ${closure.maximumNumberOfParameters}")
/ / determine the delegate
if(closure.delegate ! = closure.owner) { println('I have a custom delegate')}// Iterate over the closure's argument types
for (paramType in closure.parameterTypes) {
if (paramType.name == 'java.util.Date') {
println('Force today')}else {
println(paramType.name)
}
}
}
execute {} // An Object parameter
execute { it } // A default argument of type Object
execute { -> } // No arguments
execute { String value -> println(value) } // A String argument
execute {int a, Date b -> println('Two params')} // Two arguments, int and Date
class A {
String name
}
Closure closure = { println(it) }
closure.delegate = new A(name: 'Tom')
execute(closure)
Copy the code
Checking closures dynamically makes method logic richer and more flexible.
Characteristic use of closures
The previous article detailed the basic concepts, definition, usage, and important delegate strategies of Groovy closures, which have initially demonstrated the power of closures. Closures are one of Groovy’s most important features. What else are interesting about closures?
It’s going to be used in GStrings
In the previous example, we used a lot of parentheses in strings, such as “My name is ${name}”. Let’s start with an example.
def x = 1
${x} is not a closure, but an expression
def gs = "x = ${x}" // The value of gs has been determined
assert gs == 'x = 1'
x = 2
// Assert gs == 'x = 2
println(gs)
Copy the code
Unfortunately, this is not a closure, but just an expression: $x, whose value was determined when GString was created. So, when x is assigned to 2, the value of gs does not change.
So how do you implement lazy loading of closures in GString? You have to use the ${-> x} format, which explicitly declares ->, to be a closure in GString.
def y = 1
${-> y} is a closure
def gsy = "y = ${-> y}"
assert gsy == 'y = 1'
y = 2
assert gsy == 'y = 2' // will reuse the y value, y = 2
Copy the code
In fact, GString only delays the evaluation of the toString representation for value changes, not reference changes.
// Define a class that contains the toString method
class Dog {
String name
String toString() { name }
}
def sam = new Dog(name:'Sam')
def lucy = new Dog(name:'Lucy')
def p = sam // Assign a value to p
def gsDog = "Name: ${p}" // The value of gsDog has been determined
assert gsDog == 'Name: Sam'
p = lucy // Although the reference to p has changed, it is not valid for gsDog
assert gsDog == 'Name: Sam'
sam.name = 'Lucy' // The value of name has changed, affecting the value of gsDog
assert gsDog == 'Name: Lucy'
// If you change it to a closure, it will be valid for references
def sam2 = new Dog(name: 'Sam2')
def lucy2 = new Dog(name: 'Lucy2')
def q = sam2
def gsDog2 = "Name: ${-> q}" // Use closures
assert gsDog2 == 'Name: Sam2'
q = lucy2 // The reference to q has changed and is still valid for gsDog
assert gsDog2 == 'Name: Lucy2'
Copy the code
Strong rotation of closures
Closures can be strong-cast into SAM types in Groovy. An SAM type is an interface or abstract class that contains only one abstract method. We can implement strong turns with the AS keyword.
// SAM type interfaces and abstract classes
interface Predicate<T> {
boolean accept(T obj)
}
abstract class Greeter {
abstract String getName()
void greet() {
println "Hello, $name"}}// Use the as keyword
Predicate filter = { it.contains 'G' } as Predicate
assert filter.accept('Groovy') = =true
Greeter greeter = { 'Groovy' } as Greeter
greeter.greet()
println(greeter.getName())
// Versions after 2.2.0 can omit the as keyword
Predicate filter2 = { it.contains 'G' }
assert filter2.accept('Groovy') = =true
Greeter greeter2 = { 'Groovy' }
greeter2.greet()
Copy the code
The as keyword can be omitted from Groovy versions 2.2.0 and above.
In fact, closures can be cast to any type except SAM, especially interfaces.
Closures can be cast to any class, especially interfaces
//interface FooBar {
// int foo()
// void bar()
/ /}
class FooBar {
int foo() { 1 }
void bar() { println 'barn'}}def impl = { println 'ok'; 123 } as FooBar
assert impl.foo() == 123 // true
impl.bar() / / print ok
Copy the code
Curring
Curring in Chinese is currization, a concept of functional programming. But currings in Groovy don’t really fit the concept of Curring, because closures in Groovy have different scoping rules. Using the Curring operation in Groovy, you can assign values to one or more of the Closure’s arguments list and return a new Closure.
def nCopies = { int n, String str -> str*n }
def twice = nCopies.curry(2) // Assign the leftmost argument
assert twice('bla') = ='blabla'
assert twice('bla') == nCopies(2.'bla')
def blah = nCopies.rcurry('bla') // Assign the rightmost argument
assert blah(2) = ='blabla'
assert blah(2) == nCopies(2.'bla')
def volume = { double l, double w, double h -> l*w*h }
def fixedWidthVolume = volume.ncurry(1.2d) // Specify an argument at a location to assign to, specifying the second argument here
assert volume(3d, 2d, 4d) == fixedWidthVolume(3d, 4d)
def fixedWidthAndHeight = volume.ncurry(1.2d, 4d) // The argument after the second argument
assert volume(3d, 2d, 4d) == fixedWidthAndHeight(3d)
def oneCurring = { item -> println(item)} // Support a case of one parameter
def oneCurring2 = oneCurring.curry("hello")
oneCurring2()
def funCurring = {int a, int b, int c, int d -> a * b * c * d}
def funCurring2 = funCurring.ncurry(1.2.3)
assert funCurring(1.2.3.4) == funCurring2(1.4)
Copy the code
Memoization
Groovy’s closures have the ability to be mnemonized, which can be interpreted as caching the results returned by calling closures. This feature can help recursion algorithms that require repeated computations, such as the Fibonacci sequence.
def fib
fib = { long n -> n < 2 ? n : fib(n - 1) + fib(n - 2)}// Some values are double-counted
assert fib(15) = =610 // slow!
Copy the code
When you compute fib of 15, you compute FIB of 14 and fib of 13, and then you compute FIB of 13 and fib of 12. Obviously fib of 13 is double-counted. Using the closure’s Memoize method avoids these double calculations.
fib = { long n -> n < 2 ? n : fib(n - 1) + fib(n - 2) }.memoize()
assert fib(25) = =75025 // fast!
Copy the code
In addition to the memoize method, closures provide several methods to control the number of caches using the LRU policy.
- MemoizeAtMost: Can be set to cache up to N values and will generate a new Closure;
- MemoizeAtLeast: You can create a new Closure with a minimum of N values in the memoizeAtLeast;
- MemoizeBetween: Can set the number range of caches and will generate a new Closure;
fib = { long n -> n < 2 ? n : fib(n - 1) + fib(n - 2) }.memoizeAtMost(8)
assert fib(25) = =75025 // fast!
fib = { long n -> n < 2 ? n : fib(n - 1) + fib(n - 2) }.memoizeAtLeast(3)
assert fib(25) = =75025 // fast!
fib = { long n -> n < 2 ? n : fib(n - 1) + fib(n - 2) }.memoizeBetween(3.8)
assert fib(25) = =75025 // fast!
Copy the code
It is important to note that the closure cache works with the actual values of the parameters. This means you need to be extra careful if you are memorizing with something other than basic or package types.
Trampoline
The Trampoline feature is provided specifically for recursive calls. The biggest pitfall when using recursion is the stack limit, which can cause StackOverflowExceptions if the recursion is too deep. The Trampoline nature of closures helps recursive methods avoid this exception (tail recursion).
When we use the trampoline() method, we wrap the current Closure as a TrampolineClosure. When called, the TrampolineClosure instance is returned and the Call method is called, and the logic of the original closure is executed. If there is a recursive call, the call method returns another instance of TrampolineClosure, triggering the logic of the original closure. This loop continues until a non-trampolineclosure value is returned. This converts the original recursive stack call into a continuous call to the method, thus avoiding stack overflow.
def factorial
factorial = { int n, def accu = 1G ->
if (n < 2) return accu
factorial.trampoline(n - 1, n * accu)
}
factorial = factorial.trampoline() // TrampolineClosure
assert factorial(1) = =1
assert factorial(3) = =1 * 2 * 3
assert factorial(1000) / / = = 402387260.. plus another 2560 digits
Copy the code
Composition
Composition of closures is straightforward compared to other features and can be understood as a combination of method calls. For example, there are functions f and g, which can form a new function h = f(g()) or h = g(f()), etc. Here’s an example:
def plus2 = { it + 2 }
def times3 = { it * 3 }
// Evaluate the tail closure of the arrow and pass its value into the head closure of the arrow
def times3plus2 = plus2 << times3 // plus2(times3())
assert times3plus2(3) = =11
assert times3plus2(4) == plus2(times3(4))
def plus2times3 = times3 << plus2 // times3(plus2())
assert plus2times3(3) = =15
assert plus2times3(5) == times3(plus2(5))
// reverse composition
def times3plus22 = times3 >> plus2
assert times3plus2(3) == times3plus22(3) // plus2(times3())
println(times3plus22(2)) / / 8
Copy the code
conclusion
This article introduced closures in Groovy, detailing their definition rules, delegation mechanisms, and use of features. All of this already shows the charm of closures, but it goes far beyond that. Where closures are really powerful is in the DSL world. If you are interested, you can preview it on your own. You will use DSLS in future Gradle articles.
The resources
- Currization function
- Closure in the true sense
- Groovy official website
- Tail recursion
- LRU algorithm
- Lambda expressions
- Groovy Programming