Groovy (Jenkins, Gradle and Spock) Module 3 – Closures: the engine behind every Groovy DSL
Closures: syntax, parameters and return
Write Groovy closures with it, named, default and varargs parameters, see why return inside each never stops the loop, and pass methods as closures.
What you will learn
- Define and call closures with the implicit it, named, default and varargs parameters
- Predict what a closure call returns, and what a call with the wrong number of arguments does
- Explain why return inside each never stops the loop, and choose find, any, every or a for loop instead
- Convert methods into closures with .& and ::, and state which runtimes accept each form
- Pass closures where Java expects a functional interface such as Comparator, Runnable or Predicate
Before you start
What you’ll learn · Before you start
What you will learn
- Define and call closures with the implicit it, named, default and varargs parameters
- Predict what a closure call returns, and what a call with the wrong number of arguments does
- Explain why return inside each never stops the loop, and choose find, any, every or a for loop instead
- Convert methods into closures with .& and ::, and state which runtimes accept each form
- Pass closures where Java expects a functional interface such as Comparator, Runnable or Predicate
Before you start
On this page
[7, 12, 9, 14].each { if (it % 2 == 0) return it } reads like a search that stops at 12, yet the code in braces runs
four times and nothing stops. That code in braces is a closure: A block of code in braces that can be stored in a variable, passed to a method and called later. In Groovy, braces after = always start a closure. More in “Groovy for Java developers: what changes”: a block that Groovy keeps as
an object, hands to methods such as each, find and collect, and runs only when something calls it. Jenkinsfiles
and Gradle build scripts are built out of closures, and Spock tests use them too.
You will write closures with every kind of parameter, predict what a call returns, stop a search with a method that really stops, and turn methods into closures and closures into Java’s functional interfaces. Each program ran on the Groovy runtimes this track pins, from the groovy-all 2.4.21 jar that Jenkins bundles up to 6.0.0, and every session names the ones it used.
A closure is code kept as an object until you call it
def announce = { println ' the closure body runs now' } prints nothing. The braces build an object of the class
groovy.lang.Closure that holds the code, and the variable announce refers to it. The body runs each time the
closure is called, as announce() or announce.call(), and a method that receives a closure calls it as often as it
needs: collect calls it once per item. Unlike a method, the Groovy documentation points out, a closure “always
returns a value”: the value of the last expression its body evaluated. It needs no return, and an empty closure
gives null. Step through the life of the closure twice in the program below:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- Defining runs nothing. def twice = { it * 2 } builds an object of the class groovy.lang.Closure that holds the code, and the variable twice refers to it. Nothing has been doubled yet.
- A call runs the body. twice(21) runs the body once, with the implicit parameter it set to 21. The closure gives back the value of its last expression, 42, without any return statement.
- A method calls it too. collect(twice) passes the same object to collect, which calls it once for each of 3, 5 and 8.
- One value per call. collect keeps what each call returned, so the session printed [6, 10, 16]. The closure was stored, called by name and passed on, and it stayed one object throughout.
// A closure is code kept as an object: its body runs only when it is called.
def announce = { println ' the closure body runs now' }
println 'announce is defined'
announce()
announce.call()
def twice = { it * 2 }
println "a Closure: ${twice instanceof Closure}"
println "called: ${twice(21)}"
println "passed on: ${[3, 5, 8].collect(twice)}"
// No return statement: a closure gives back the last value it computed.
def verdict = { int minutes ->
if (minutes > 30) {
'slow build'
} else {
'fast build'
}
}
println "last value: ${verdict(42)}, ${verdict(7)}"
def nothing = { }
println "empty body: ${nothing()}" from session import jenkins_jar, run, use
# A closure as an object: defined, called and passed on (basics.groovy).
# It runs on Groovy 6.0.0 first, then on each older runtime; diff prints only the lines that differ from 6.0.0's.
run("groovy basics.groovy | tee on-6.0.0.txt")
for version in ("5.1.3", "4.0.33"):
use(version)
run(f"""
groovy basics.groovy > on-{version}.txt
diff on-6.0.0.txt on-{version}.txt && echo 'Groovy {version} printed the same lines' || echo 'Groovy {version} differs in the lines above'
""")
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain basics.groovy > on-2.4.21.txt
diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines' || echo 'Groovy 2.4.21 differs in the lines above'
""") Output
$ groovy basics.groovy | tee on-6.0.0.txt announce is defined the closure body runs now the closure body runs now a Closure: true called: 42 passed on: [6, 10, 16] last value: slow build, fast build empty body: null # The groovy command is Groovy 5.1.3 from here on. $ groovy basics.groovy > on-5.1.3.txt $ diff on-6.0.0.txt on-5.1.3.txt && echo 'Groovy 5.1.3 printed the same lines' || echo 'Groovy 5.1.3 differs in the lines above' Groovy 5.1.3 printed the same lines # The groovy command is Groovy 4.0.33 from here on. $ groovy basics.groovy > on-4.0.33.txt $ diff on-6.0.0.txt on-4.0.33.txt && echo 'Groovy 4.0.33 printed the same lines' || echo 'Groovy 4.0.33 differs in the lines above' Groovy 4.0.33 printed the same lines # groovy-all-2.4.21.jar in this folder is the Groovy jar that Jenkins bundles, from Maven Central. $ java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain basics.groovy > on-2.4.21.txt $ diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines' || echo 'Groovy 2.4.21 differs in the lines above' Groovy 2.4.21 printed the same lines
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 object_session.py
announce is defined came first, and the line of the body appeared only with the two calls. collect(twice) called
the same object three times and kept 6, 10 and 16. verdict gave slow build for 42 and fast build for 7, the value
of whichever branch of its if ran, and the empty closure gave null. The three older runtimes printed the same lines
as 6.0.0.
Key idea
A closure is an object that holds code: defining it runs nothing, each call runs the body once, and the call gives back the last value the body computed.
Parameters: none, the implicit it, named, default and varargs
A closure lists its parameters before an arrow, { a, b -> a + b }, much as a method does: typed or untyped, with
default values, and with a varargs parameter last. Leave the arrow out and the closure still takes one parameter, the
implicit it: The name of the single parameter a Groovy closure has when it declares none, so { it * 2 } doubles its argument. A closure written { -> … } has no parameter at all.. A closure that must take nothing is written with an empty list,
{ -> … }. The program below defines one closure of each form and asks each how many arguments it accepts, with
maximumNumberOfParameters:
| Form | The closure | Parameters |
|---|---|---|
| None | { -> 42 } |
0 |
Implicit it |
{ it * 2 } |
1 |
| Named, typed | { int a, int b -> a + b } |
2 |
| With a default | { String name, String word = 'Hello' -> … } |
2 |
| Varargs | { String label, int... amounts -> … } |
2 |
// Every parameter form of a closure.
// No parameters:
def answer = { -> 42 }
// One parameter, the implicit it:
def twice = { it * 2 }
// Two typed parameters:
def add = { int a, int b -> a + b }
// A parameter with a default value:
def greet = { String name, String word = 'Hello' ->
"$word, $name"
}
// A variable number of arguments (varargs):
def total = { String label, int... amounts ->
"$label: ${amounts.sum()}"
}
println "answer() gives ${answer()}"
println "twice(8) gives ${twice(8)}"
println "add(40, 2) gives ${add(40, 2)}"
println "greet('Asha') gives ${greet('Asha')}"
println "greet('Asha', 'Namaste') gives ${greet('Asha', 'Namaste')}"
println "total('cart', 120, 80, 50) gives ${total('cart', 120, 80, 50)}"
println "total('empty') gives ${total('empty')}"
def all = [answer, twice, add, greet, total]
println "parameters: ${all*.maximumNumberOfParameters}"
println "types: ${all.collect { it.parameterTypes*.simpleName }}" from session import jenkins_jar, run, use
# Every parameter form of a closure (forms.groovy).
# It runs on Groovy 6.0.0 first, then on each older runtime; diff prints only the lines that differ from 6.0.0's.
run("groovy forms.groovy | tee on-6.0.0.txt")
for version in ("5.1.3", "4.0.33"):
use(version)
run(f"""
groovy forms.groovy > on-{version}.txt
diff on-6.0.0.txt on-{version}.txt && echo 'Groovy {version} printed the same lines' || echo 'Groovy {version} differs in the lines above'
""")
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain forms.groovy > on-2.4.21.txt
diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines' || echo 'Groovy 2.4.21 differs in the lines above'
""") Output
$ groovy forms.groovy | tee on-6.0.0.txt
answer() gives 42
twice(8) gives 16
add(40, 2) gives 42
greet('Asha') gives Hello, Asha
greet('Asha', 'Namaste') gives Namaste, Asha
total('cart', 120, 80, 50) gives cart: 250
total('empty') gives empty: 0
parameters: [0, 1, 2, 2, 2]
types: [[], [Object], [int, int], [String, String], [String, int[]]]
# The groovy command is Groovy 5.1.3 from here on.
$ groovy forms.groovy > on-5.1.3.txt
$ diff on-6.0.0.txt on-5.1.3.txt && echo 'Groovy 5.1.3 printed the same lines' || echo 'Groovy 5.1.3 differs in the lines above'
Groovy 5.1.3 printed the same lines
# The groovy command is Groovy 4.0.33 from here on.
$ groovy forms.groovy > on-4.0.33.txt
$ diff on-6.0.0.txt on-4.0.33.txt && echo 'Groovy 4.0.33 printed the same lines' || echo 'Groovy 4.0.33 differs in the lines above'
Groovy 4.0.33 printed the same lines
# groovy-all-2.4.21.jar in this folder is the Groovy jar that Jenkins bundles, from Maven Central.
$ java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain forms.groovy > on-2.4.21.txt
$ diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines' || echo 'Groovy 2.4.21 differs in the lines above'
Groovy 2.4.21 printed the same lines
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 forms_session.py
greet('Asha') filled in the default and gave Hello, Asha, and total took three amounts, or none at all, without
the caller building an array. The counts were 0, 1, 2, 2 and 2: a default value does not lower the count, and the
varargs parameter counts once, as the int[] among its parameter types shows. All four runtimes agreed.
A call with the wrong number of arguments fails in one of two ways, and the second fails late. answer takes no
parameter, so answer(1) has nowhere to put the 1. twice takes one, so twice() runs with it set to null:
// Calling closures with the wrong number of arguments.
def answer = { -> 42 }
def twice = { it * 2 }
try {
answer(1)
} catch (MissingMethodException e) {
println "answer(1): ${e.message.readLines()[0]}"
}
try {
twice()
} catch (NullPointerException e) {
println "twice(): ${e.message}"
} from session import jenkins_jar, run, use
# Closures called with the wrong number of arguments (arity.groovy).
# It runs on Groovy 6.0.0 first, then on each older runtime; diff prints only the lines that differ from 6.0.0's.
run("groovy arity.groovy | tee on-6.0.0.txt")
for version in ("5.1.3", "4.0.33"):
use(version)
run(f"""
groovy arity.groovy > on-{version}.txt
diff on-6.0.0.txt on-{version}.txt && echo 'Groovy {version} printed the same lines' || echo 'Groovy {version} differs in the lines above'
""")
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain arity.groovy > on-2.4.21.txt
diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines' || echo 'Groovy 2.4.21 differs in the lines above'
""") Output
$ groovy arity.groovy | tee on-6.0.0.txt answer(1): No signature of method: call for class: arity$_run_closure1 is applicable for argument types: (Integer) values: [1] twice(): Cannot invoke method multiply() on null object # The groovy command is Groovy 5.1.3 from here on. $ groovy arity.groovy > on-5.1.3.txt $ diff on-6.0.0.txt on-5.1.3.txt && echo 'Groovy 5.1.3 printed the same lines' || echo 'Groovy 5.1.3 differs in the lines above' Groovy 5.1.3 printed the same lines # The groovy command is Groovy 4.0.33 from here on. $ groovy arity.groovy > on-4.0.33.txt $ diff on-6.0.0.txt on-4.0.33.txt && echo 'Groovy 4.0.33 printed the same lines' || echo 'Groovy 4.0.33 differs in the lines above' 1c1 < answer(1): No signature of method: call for class: arity$_run_closure1 is applicable for argument types: (Integer) values: [1] --- > answer(1): No signature of method: arity$_run_closure1.call() is applicable for argument types: (Integer) values: [1] Groovy 4.0.33 differs in the lines above # groovy-all-2.4.21.jar in this folder is the Groovy jar that Jenkins bundles, from Maven Central. $ java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain arity.groovy > on-2.4.21.txt $ diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines' || echo 'Groovy 2.4.21 differs in the lines above' 1c1 < answer(1): No signature of method: call for class: arity$_run_closure1 is applicable for argument types: (Integer) values: [1] --- > answer(1): No signature of method: arity$_run_closure1.call() is applicable for argument types: (java.lang.Integer) values: [1] Groovy 2.4.21 differs in the lines above
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 arity_session.py
answer(1) threw MissingMethodException: The runtime error of dynamic Groovy when no method of the called name accepts the given arguments. Its message names the method, the class and the argument types, and lists Possible solutions. More in “Reading Groovy errors and stack traces” on every runtime, worded two ways:
6.0.0 and 5.1.3 say No signature of method: call for class: …, while 4.0.33 and 2.4.21 name the call() method of
the closure’s class, as the lines that diff printed show. twice() got as far as the body and failed there, at
null * 2, with Cannot invoke method multiply() on null object.
A callback that quietly accepts an argument
{ println 'done' } takes the implicit it, so a caller that passes a value by mistake goes unnoticed. When a
closure must take no argument, write { -> println 'done' }: a stray argument then fails at the call.
Checkpoint
Check: A closure as an object that returns its last value · Parameter forms and argument counts
return ends one call of the closure, not the loop
each calls its closure once for every element and does nothing with what the closure returns. A return inside the
closure therefore ends only the current call, and each carries on with the next element. That is why the
firstEven method below returns none for [7, 12, 9, 14], a list that holds two even numbers. Step through the
call:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- The method calls each. firstEven hands its closure to each, which will call it once for every number of the list, whatever the closure returns.
- 7 is odd. each calls the closure with n = 7. The if is false, so the call ends with no value of its own, null.
- 12 is even. each calls the closure with n = 12, and return n runs. return ends this call of the closure and hands 12 to its caller, which is each, not firstEven.
- each ignores 12. each does not look at the value a call returns. It takes the next number, so nothing has stopped.
- 9 and 14. The closure runs twice more, for 9 and 14. That makes four calls in all, as the session counted.
- each is done. each has been through the whole list, and firstEven moves on to its next line.
- The method returns none. return 'none' is a statement of the method itself, so firstEven returns none. find would have stopped at 12 after two calls and returned 12.
// Looking for the first even build number in a list.
def firstEven(List numbers) {
numbers.each { n ->
if (n % 2 == 0) return n // ends this call of the closure
}
return 'none'
}
println "each + return: ${firstEven([7, 12, 9, 14])}"
int calls = 0
[7, 12, 9, 14].each { n -> calls++; if (n % 2 == 0) return }
println "each: the closure ran $calls times"
calls = 0
def found = [7, 12, 9, 14].find { n -> calls++; n % 2 == 0 }
println "find: $found after $calls calls"
calls = 0
def anyEven = [7, 12, 9, 14].any { n -> calls++; n % 2 == 0 }
println "any: $anyEven after $calls calls"
calls = 0
def allOdd = [7, 12, 9, 14].every { n -> calls++; n % 2 == 1 }
println "every: $allOdd after $calls calls"
def kept = [7, 12, 9, 14].collect { n -> if (n % 2 == 0) return n }
println "collect keeps each call's value: $kept"
def firstEvenLoop(List numbers) {
for (n in numbers) {
if (n % 2 == 0) return n // ends the method
}
return 'none'
}
println "for + return: ${firstEvenLoop([7, 12, 9, 14])}" from session import jenkins_jar, run, use
# return inside each, and the methods that stop early (firstEven.groovy).
# It runs on Groovy 6.0.0 first, then on each older runtime; diff prints only the lines that differ from 6.0.0's.
run("groovy firstEven.groovy | tee on-6.0.0.txt")
for version in ("5.1.3", "4.0.33"):
use(version)
run(f"""
groovy firstEven.groovy > on-{version}.txt
diff on-6.0.0.txt on-{version}.txt && echo 'Groovy {version} printed the same lines' || echo 'Groovy {version} differs in the lines above'
""")
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain firstEven.groovy > on-2.4.21.txt
diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines' || echo 'Groovy 2.4.21 differs in the lines above'
""") Output
$ groovy firstEven.groovy | tee on-6.0.0.txt each + return: none each: the closure ran 4 times find: 12 after 2 calls any: true after 2 calls every: false after 2 calls collect keeps each call's value: [null, 12, null, 14] for + return: 12 # The groovy command is Groovy 5.1.3 from here on. $ groovy firstEven.groovy > on-5.1.3.txt $ diff on-6.0.0.txt on-5.1.3.txt && echo 'Groovy 5.1.3 printed the same lines' || echo 'Groovy 5.1.3 differs in the lines above' Groovy 5.1.3 printed the same lines # The groovy command is Groovy 4.0.33 from here on. $ groovy firstEven.groovy > on-4.0.33.txt $ diff on-6.0.0.txt on-4.0.33.txt && echo 'Groovy 4.0.33 printed the same lines' || echo 'Groovy 4.0.33 differs in the lines above' Groovy 4.0.33 printed the same lines # groovy-all-2.4.21.jar in this folder is the Groovy jar that Jenkins bundles, from Maven Central. $ java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain firstEven.groovy > on-2.4.21.txt $ diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines' || echo 'Groovy 2.4.21 differs in the lines above' Groovy 2.4.21 printed the same lines
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 return_session.py
The counters show the difference. The closure given to each ran 4 times. find returned 12 after 2 calls, any
gave true after 2 and every gave false after 2: each of them stops at the first call whose result settles the
answer. collect shows where the value of a return goes, since it keeps each call’s value: [null, 12, null, 14].
Only the return of the for loop left the method, with 12. Every runtime printed the same lines. Choose the method
by what you need:
| To get | Write | It stops at |
|---|---|---|
| The first match | list.find { … } |
the first true result |
| Whether one matches | list.any { … } |
the first true result |
| Whether all match | list.every { … } |
the first false result |
| Out of the method | for (x in list) and return |
the return |
| Every element | list.each { … } |
nowhere: it visits all |
break is no way out either. It belongs to loops and switches, a closure body is neither, and the compiler says so:
// break belongs to loops and switches, and a closure body is neither.
[7, 12, 9, 14].each { n ->
if (n % 2 == 0) break
println n
} from session import jenkins_jar, run
# break inside a closure given to each (eachBreak.groovy), on Groovy 6.0.0 and on the jar that Jenkins bundles.
run("!groovy eachBreak.groovy")
jenkins_jar()
run("!java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain eachBreak.groovy") Output
$ groovy eachBreak.groovy
… groovy.ui.GroovyMain run
SEVERE: org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
eachBreak.groovy: 3: the break statement is only allowed inside loops or switches
@ line 3, column 21.
if (n % 2 == 0) break
^
1 error
[exit status 1]
# groovy-all-2.4.21.jar in this folder is the Groovy jar that Jenkins bundles, from Maven Central.
$ java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain eachBreak.groovy
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
eachBreak.groovy: 3: the break statement is only allowed inside loops or switches
@ line 3, column 21.
if (n % 2 == 0) break
^
1 error
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 break_session.py
Key idea
Inside each, return ends one call of the closure. To stop at the first match, use find, any or every, or
a for loop, whose return leaves the method.
Checkpoint
Check: return inside each, and the methods that stop
Method pointers and references turn methods into closures
The method pointer operator .& makes a closure out of a method. 'deploy'.&toUpperCase is a
method pointer: A closure made from a method with the .& operator: 'deploy'.&toUpperCase calls toUpperCase on that string. From Groovy 3, String.&trim takes the object as its first argument; on groovy-all 2.4.21 that unbound form fails when called. bound to one string: calling it calls toUpperCase on 'deploy', and
since it is a Closure it goes wherever a closure goes. The operators chapter of the Groovy documentation describes
two more forms, both new in Groovy 3: a pointer without an object, String.&trim, which takes the object as its first
argument, and Java’s method reference: Java's Class::method syntax, which Groovy accepts from version 3. In dynamic Groovy it is another way to write the method pointer .&, so String::toUpperCase gives a Closure. operator ::, which in dynamic Groovy “is
just an alias” for .&. The Groovy 3 release notes say the same of lambdas such as (int n) -> n * 2: in dynamic
Groovy they are turned into closures.
// Methods as closures, with the method pointer operator .&
def shout = 'deploy'.&toUpperCase
println "bound: ${shout()}, a Closure: ${shout instanceof Closure}"
def isRelease = 'release-6.0'.&startsWith
println "bound, with an argument: ${isRelease('release')}"
def jobs = [' build ', ' qa']
println "a closure: ${jobs.collect { it.trim() }}"
println "unbound: ${jobs.collect(String.&trim)}" from session import jenkins_jar, run, use
# Methods turned into closures with .& (pointers.groovy).
# It runs on Groovy 6.0.0 first, then on each older runtime; diff prints only the lines that differ from 6.0.0's.
run("groovy pointers.groovy | tee on-6.0.0.txt")
for version in ("5.1.3", "4.0.33"):
use(version)
run(f"""
groovy pointers.groovy > on-{version}.txt
diff on-6.0.0.txt on-{version}.txt && echo 'Groovy {version} printed the same lines' || echo 'Groovy {version} differs in the lines above'
""")
# On the jar that Jenkins bundles the unbound pointer fails: its error is printed, and the lines before it go to the file.
jenkins_jar()
run("""
!java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain pointers.groovy > on-2.4.21.txt
diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines' || echo 'Groovy 2.4.21 differs in the lines above'
""") Output
$ groovy pointers.groovy | tee on-6.0.0.txt bound: DEPLOY, a Closure: true bound, with an argument: true a closure: [build, qa] unbound: [build, qa] # The groovy command is Groovy 5.1.3 from here on. $ groovy pointers.groovy > on-5.1.3.txt $ diff on-6.0.0.txt on-5.1.3.txt && echo 'Groovy 5.1.3 printed the same lines' || echo 'Groovy 5.1.3 differs in the lines above' Groovy 5.1.3 printed the same lines # The groovy command is Groovy 4.0.33 from here on. $ groovy pointers.groovy > on-4.0.33.txt $ diff on-6.0.0.txt on-4.0.33.txt && echo 'Groovy 4.0.33 printed the same lines' || echo 'Groovy 4.0.33 differs in the lines above' Groovy 4.0.33 printed the same lines # groovy-all-2.4.21.jar in this folder is the Groovy jar that Jenkins bundles, from Maven Central. $ java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain pointers.groovy > on-2.4.21.txt Caught: groovy.lang.MissingMethodException: No signature of method: java.lang.String.trim() is applicable for argument types: (java.lang.String) values: [ build ] Possible solutions: trim(), strip(), wait(), grep(), wait(long), split(java.lang.String) groovy.lang.MissingMethodException: No signature of method: java.lang.String.trim() is applicable for argument types: (java.lang.String) values: [ build ] Possible solutions: trim(), strip(), wait(), grep(), wait(long), split(java.lang.String) at pointers.run(pointers.groovy:10) [exit status 1] $ diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines' || echo 'Groovy 2.4.21 differs in the lines above' 4d3 < unbound: [build, qa] Groovy 2.4.21 differs in the lines above
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pointers_session.py
The bound pointers worked everywhere. The unbound String.&trim trimmed both names on the three newer runtimes,
while the groovy-all 2.4.21 jar looked for a trim that takes the name as an argument and stopped with
MissingMethodException; diff shows the line it never printed. The method reference and the lambda of the next
program fare worse there:
// The Groovy 3 forms: a method reference and a lambda.
def upper = String::toUpperCase
println "method reference: ${['build', 'qa'].collect(upper)}"
println "is a Closure: ${upper instanceof Closure}"
def twice = (int n) -> n * 2
println "lambda: ${twice(21)}, is a Closure: ${twice instanceof Closure}" from session import jenkins_jar, run, use
# The Groovy 3 forms, a method reference and a lambda (references.groovy).
# It runs on Groovy 6.0.0 first, then on each older runtime; diff prints only the lines that differ from 6.0.0's.
run("groovy references.groovy | tee on-6.0.0.txt")
for version in ("5.1.3", "4.0.33"):
use(version)
run(f"""
groovy references.groovy > on-{version}.txt
diff on-6.0.0.txt on-{version}.txt && echo 'Groovy {version} printed the same lines' || echo 'Groovy {version} differs in the lines above'
""")
# The jar that Jenkins bundles does not compile the file.
jenkins_jar()
run("!java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain references.groovy") Output
$ groovy references.groovy | tee on-6.0.0.txt
method reference: [BUILD, QA]
is a Closure: true
lambda: 42, is a Closure: true
# The groovy command is Groovy 5.1.3 from here on.
$ groovy references.groovy > on-5.1.3.txt
$ diff on-6.0.0.txt on-5.1.3.txt && echo 'Groovy 5.1.3 printed the same lines' || echo 'Groovy 5.1.3 differs in the lines above'
Groovy 5.1.3 printed the same lines
# The groovy command is Groovy 4.0.33 from here on.
$ groovy references.groovy > on-4.0.33.txt
$ diff on-6.0.0.txt on-4.0.33.txt && echo 'Groovy 4.0.33 printed the same lines' || echo 'Groovy 4.0.33 differs in the lines above'
Groovy 4.0.33 printed the same lines
# groovy-all-2.4.21.jar in this folder is the Groovy jar that Jenkins bundles, from Maven Central.
$ java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain references.groovy
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
references.groovy: 2: expecting EOF, found ':' @ line 2, column 19.
def upper = String::toUpperCase
^
1 error
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 references_session.py
From 4.0.33 on, String::toUpperCase gave [BUILD, QA], and both the reference and the lambda were Closure
objects. Groovy 2.4.21 did not compile the file at all: expecting EOF, found ':', at the :: of line 2. The five
ways to pass behaviour, side by side:
| Form | 2.4.21 (Jenkins) | 4.0.33, 5.1.3, 6.0.0 |
|---|---|---|
{ it.trim() } |
runs | runs |
'deploy'.&toUpperCase |
runs | runs |
String.&trim |
fails when called | runs |
String::toUpperCase |
compile error | runs |
(int n) -> n * 2 |
compile error | runs |
Version note
Jenkins 2.580.1 bundles groovy-all 2.4.21, as its bill of materials lists, so code that Jenkins runs needs a closure
literal or a bound pointer. Unbound pointers, :: and lambdas need Groovy 3 or later, which the other three
runtimes of this track have.
A closure can stand in for a Java functional interface
Java libraries take behaviour as an object of an interface with one abstract method, such as Comparator,
Runnable, Function or Predicate: a SAM type: An interface or abstract class with a single abstract method, such as Comparator, Runnable or Predicate. Groovy converts a closure into one when the closure is assigned to that type or passed where it is expected.. Groovy turns a closure into such an
object when you assign the closure to a variable of that type, or write as and the type. The semantics chapter of
the documentation says the as is optional since Groovy 2.2.0, also when the closure is passed straight to a method
that expects the interface:
// A closure where Java expects an interface with one abstract method.
import java.util.function.Function
import java.util.function.Predicate
Comparator<String> byLength = { a, b -> a.size() <=> b.size() }
println "Comparator: ${['deploy', 'qa', 'test'].sort(false, byLength)}"
Runnable job = { println 'Runnable: ran' }
job.run()
Function<Integer, Integer> doubler = { it * 2 }
println "Function: ${doubler.apply(21)}"
def isRerun = { String s -> s.startsWith('re') } as Predicate
println "Predicate: ${isRerun.test('rerun')}, ${isRerun instanceof Predicate}"
// Java methods that take an interface, given a closure directly:
def names = ['test', 'build', 'qa']
names.removeIf { it.size() < 3 }
println "removeIf: $names"
def sorted = ['release', 'qa', 'test'].toSorted { a, b -> a.size() <=> b.size() }
println "toSorted: $sorted" from session import jenkins_jar, run, use
# Closures where Java expects a functional interface (sam.groovy).
# It runs on Groovy 6.0.0 first, then on each older runtime; diff prints only the lines that differ from 6.0.0's.
run("groovy sam.groovy | tee on-6.0.0.txt")
for version in ("5.1.3", "4.0.33"):
use(version)
run(f"""
groovy sam.groovy > on-{version}.txt
diff on-6.0.0.txt on-{version}.txt && echo 'Groovy {version} printed the same lines' || echo 'Groovy {version} differs in the lines above'
""")
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain sam.groovy > on-2.4.21.txt
diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines' || echo 'Groovy 2.4.21 differs in the lines above'
""") Output
$ groovy sam.groovy | tee on-6.0.0.txt Comparator: [qa, test, deploy] Runnable: ran Function: 42 Predicate: true, true removeIf: [test, build] toSorted: [qa, test, release] # The groovy command is Groovy 5.1.3 from here on. $ groovy sam.groovy > on-5.1.3.txt $ diff on-6.0.0.txt on-5.1.3.txt && echo 'Groovy 5.1.3 printed the same lines' || echo 'Groovy 5.1.3 differs in the lines above' Groovy 5.1.3 printed the same lines # The groovy command is Groovy 4.0.33 from here on. $ groovy sam.groovy > on-4.0.33.txt $ diff on-6.0.0.txt on-4.0.33.txt && echo 'Groovy 4.0.33 printed the same lines' || echo 'Groovy 4.0.33 differs in the lines above' Groovy 4.0.33 printed the same lines # groovy-all-2.4.21.jar in this folder is the Groovy jar that Jenkins bundles, from Maven Central. $ java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain sam.groovy > on-2.4.21.txt $ diff on-6.0.0.txt on-2.4.21.txt && echo 'Groovy 2.4.21 printed the same lines' || echo 'Groovy 2.4.21 differs in the lines above' Groovy 2.4.21 printed the same lines
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sam_session.py
sort took the closure as a Comparator and ordered the names by length, the Runnable ran, the Function doubled
21, and the closure made with as Predicate was a real Predicate. removeIf, a Java method of every collection,
received a bare closure and removed qa, and toSorted ordered three names by length. Groovy 2.4.21 printed the same
lines as 6.0.0.
The same comparator inside a Jenkinsfile
Jenkins rewrites the Groovy of a Pipeline with its CPS transformation: The continuation-passing style rewrite that the Jenkins Pipeline plugin applies to Pipeline Groovy, so that Jenkins can save a running build's program state to disk and resume it after a restart. More in “Groovy in 2026: one language, three runtimes”, and the
closures you write there are rewritten too. The Jenkins page on CPS method mismatches shows a comparator closure
given to toSorted, which is not rewritten, returning -1, “the return value of the first call to the closure”,
instead of a sorted list. Plain Groovy 2.4.21 sorted correctly above; a later module covers the rules of Pipeline
code.
Checkpoint
Check: Method pointers, method references and lambdas · Closures as Java functional interfaces
Practise: closures, return and method pointers
These questions cover the five ideas of this lesson, from what a call returns to a comparator in a Jenkinsfile. The idea you have answered least well comes first, and every answer is explained from the runs above:
Practice that adapts to you
Practise this lesson
15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What is a closure in Groovy, and how does it differ from a method?
A closure is a block of code in braces that Groovy turns into an object of the class groovy.lang.Closure. Because it
is an object, it can be stored in a variable, passed to a method such as each or collect, returned from a method
and called later, as often as needed; its body runs only when it is called. A method belongs to a class and is called
by its name. A closure takes its parameters before an arrow, has the implicit parameter it when it declares none,
and always returns a value, the last expression it evaluated. Inside a closure, return ends that call only, so a
return inside each neither stops the loop nor leaves the method around it. A method becomes a closure with .&,
and a closure can be passed where Java expects an interface such as Comparator.
Two longer questions of this module start from this lesson:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A pipeline helper should return the first failed stage. It is written stages.each { if (it.failed) return it }, followed by return null, and it always returns null, although a colleague says the loop stops at the failure. Explain what really happens, fix the helper, and say how a test would have caught it.
-
Mid to senior
A shared library cleans names with names.collect(String.&trim). Its unit tests run on Groovy 4.0.33 and pass, but in Jenkins the call fails with MissingMethodException: No signature of method: java.lang.String.trim() is applicable for argument types: (java.lang.String). Explain why, and give a fix that works in both places.
Key takeaways
- A closure is an object of the class
groovy.lang.Closure: defining it runs nothing, each call runs its body, and the call returns the last value the body computed. - Parameters go before
->; without an arrow a closure takes the implicitit, and{ -> … }takes none, so a stray argument fails at once with MissingMethodException. returninsideeachends one call of the closure, whilefind,anyandeverystop at the first result that settles the answer and aforloop’sreturnleaves the method..&and::make closures from methods; the groovy-all 2.4.21 jar that Jenkins bundles runs bound pointers only and refuses::and lambdas when it compiles.- A closure becomes a
Comparator,Runnable,FunctionorPredicatewhen it is assigned to that type or passed where one is expected, but in a Jenkinsfile a closure given to a method such astoSortedgives a wrong result.
Terms in this lesson
- it
- The name of the single parameter a Groovy closure has when it declares none, so { it * 2 } doubles its argument. A closure written { -> … } has no parameter at all.
- method pointer
- A closure made from a method with the .& operator: 'deploy'.&toUpperCase calls toUpperCase on that string. From Groovy 3, String.&trim takes the object as its first argument; on groovy-all 2.4.21 that unbound form fails when called.
- method reference
- Java's Class::method syntax, which Groovy accepts from version 3. In dynamic Groovy it is another way to write the method pointer .&, so String::toUpperCase gives a Closure.
- SAM type
- An interface or abstract class with a single abstract method, such as Comparator, Runnable or Predicate. Groovy converts a closure into one when the closure is assigned to that type or passed where it is expected.
References
- Closures (Groovy 6.0.0 documentation) (The Apache Software Foundation (Apache Groovy))
- Operators, method pointer and method reference (Groovy 6.0.0 documentation) (The Apache Software Foundation (Apache Groovy))
- Semantics, closure to type coercion (Groovy 6.0.0 documentation) (The Apache Software Foundation (Apache Groovy))
- The Groovy Development Kit API (each, find, any, every, collect) (The Apache Software Foundation (Apache Groovy))
- Release notes for Groovy 3.0 (lambdas and method references) (The Apache Software Foundation (Apache Groovy))
- Pipeline CPS method mismatches (The Jenkins project (jenkins.io))
- Jenkins 2.580.1 bill of materials (bom/pom.xml) (The Jenkins project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress