Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 10 minutes
  • Examples run with groovy 6.0.0, groovy5 5.1.3, groovyGradle 4.0.33, groovyJenkins 2.4.21, Java temurin-25.0.4+101.0.LTS, gradle 9.8.0, spock 2.4-groovy-5.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

Interactive · step by step The life of the closure twice Step through basics.groovy. Watch when the code in braces runs, and what each call gives back.
The life of the closure twice, step 4 of 4. Showing { it * 2 } (called 3 more times), twice, twice(21), 42, collect(twice), [6, 10, 16]. In focus: [6, 10, 16].refers toreturnsbuilds{ it * 2 }called 3 more timestwicea variabletwice(21)it is 2142the last valuecollect(twice)on [3, 5, 8][6, 10, 16]kept by collect

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. A method calls it too. collect(twice) passes the same object to collect, which calls it once for each of 3, 5 and 8.
  4. 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.

basics.groovy GROOVY · basics.groovy
// 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()}"
A closure defined, called and passed on, on four runtimes Python · object_session.py
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
forms.groovy GROOVY · forms.groovy
// 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 }}"
Every parameter form, on four runtimes Python · forms_session.py
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:

arity.groovy GROOVY · arity.groovy
// 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}"
}
Calls with the wrong number of arguments, on four runtimes Python · arity_session.py
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

  1. Question 1 of 2 What does this script print?

    Read the code, then choose one answer.

    def log = { println 'logged' }
    println 'start'
    [1, 2].each(log)
    Show the answer: Question 1 of 2

    Answer: start, then logged twice

    A closure is an object that holds code. Its body runs once per call, and each makes one call per element, so the output is start, logged, logged.

  2. Question 2 of 2 Which of these closures throws MissingMethodException when it is called as job(1)?

    Choose one answer.

    A hint

    Which form declares that it takes nothing at all?

    Show the answer: Question 2 of 2

    Answer: def job = { -> 'done' }

    { -> … } is the only form with zero parameters (maximumNumberOfParameters 0). Every other form accepts one argument, which is why a callback that must take nothing is written with the empty arrow.

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:

Interactive · message by message What return does inside each Step through firstEven([7, 12, 9, 14]). Watch where each return goes, and who receives it.
What return does inside each, step 7 of 7. Messages so far: firstEven to each: each { n -> … }; each to { n -> … }: n = 7; each to { n -> … }: n = 12; { n -> … } to each: returns 12; each to { n -> … }: n = 9, then 14; each to firstEven: done.firstEventhe methodeachGDK method{ n -> … }the closureeach { n -> … }n = 7n = 12returns 12n = 9, then 14done

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. each ignores 12. each does not look at the value a call returns. It takes the next number, so nothing has stopped.
  5. 9 and 14. The closure runs twice more, for 9 and 14. That makes four calls in all, as the session counted.
  6. each is done. each has been through the whole list, and firstEven moves on to its next line.
  7. 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.

firstEven.groovy GROOVY · firstEven.groovy
// 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])}"
return inside each, and the methods that stop early, on four runtimes Python · return_session.py
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:

eachBreak.groovy GROOVY · eachBreak.groovy
// 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
}
break inside each, on Groovy 6.0.0 and 2.4.21 Python · break_session.py
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

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    def firstLong(List names) {
        names.each { if (it.size() > 4) return it }
        return 'none'
    }
    println firstLong(['qa', 'deploy', 'release'])
    Show the answer: Question 1 of 2

    Answer: none

    This is firstEven of the lesson with names: the closure ran for every element, and the return inside it only ended each call.

  2. Question 2 of 2 A helper must return the first stage whose status is FAILED and look no further. Which line does that?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: stages.find { it.status == 'FAILED' }

    find, any and every stop as soon as the answer is settled; each and collect always visit every element. A for loop with return would work too.

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.

pointers.groovy GROOVY · pointers.groovy
// 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)}"
Method pointers, on four runtimes Python · pointers_session.py
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:

references.groovy GROOVY · references.groovy
// 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}"
A method reference and a lambda, on four runtimes Python · references_session.py
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:

sam.groovy GROOVY · sam.groovy
// 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"
Closures as functional interfaces, on four runtimes Python · sam_session.py
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

  1. Question 1 of 3 A shared library that Jenkins runs on groovy-all 2.4.21 must trim a list of names. Which line works there?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: names.collect { it.trim() }

    Unbound pointers, method references and lambdas need Groovy 3 or later. On the Groovy that Jenkins bundles, use a closure literal or a pointer bound to an object.

  2. Question 2 of 3 What is names after these lines?

    Read the code, then choose one answer.

    def names = ['test', 'build', 'qa']
    names.removeIf { it.size() < 3 }
    Show the answer: Question 2 of 3

    Answer: [test, build]

    A closure becomes the single method of the interface a Java method expects, here Predicate.test, so removeIf used it to decide which items to drop.

  3. Question 3 of 3 A comparator closure gives toSorted a sorted list in a Groovy 2.4.21 script, but -1 inside a Jenkinsfile. Why?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: In a Pipeline the closure is CPS-transformed while toSorted is not, so the call gives back the result of the first call to the closure

    Jenkins rewrites Pipeline Groovy, closures included, and a method that is not rewritten cannot call a rewritten closure correctly. The fix belongs to the Pipeline lessons: the Jenkins page puts such calls in a method marked @NonCPS.

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.

  1. Question 1 of 15 What is the class of the object that def twice = { it * 2 } stores in twice?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: groovy.lang.Closure, or a class that extends it

    Each closure literal is compiled into a class that extends groovy.lang.Closure, and evaluating the braces makes one object of it.

  2. Question 2 of 15 What does this print?

    Read the code, then choose one answer.

    def size = { String name ->
        if (name.size() > 5) { 'long' } else { 'short' }
    }
    println size('release')
    Show the answer: Question 2 of 15

    Answer: long

    A closure always returns a value: the value of the last expression it evaluated, here the 'long' branch of the if.

  3. Question 3 of 15 What does calling an empty closure, def nothing = { }, return?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: null

    A call gives back the last value the body computed; an empty body computes none, so the value is null.

  4. Question 4 of 15 How many arguments does { it * 2 } accept, according to its maximumNumberOfParameters?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: 1

    Leaving the arrow out gives one implicit parameter, it; an empty arrow gives none.

  5. Question 5 of 15 What does this print?

    Read the code, then choose one answer.

    def tag = { String version, String suffix = 'rc1' ->
        "$version-$suffix"
    }
    println tag('6.0.0')
    println tag('6.0.0', 'ga')
    Show the answer: Question 5 of 15

    Answer: 6.0.0-rc1, then 6.0.0-ga

    Closure parameters may have defaults, exactly as method parameters can, and a default parameter still counts in maximumNumberOfParameters.

  6. Question 6 of 15 def total = { String label, int... amounts -> amounts.sum() } is called as total('cart'). What happens?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: It returns 0, the sum of no amounts

    A varargs parameter last in the list takes any number of arguments, none included, without the caller wrapping them in an array.

  7. Question 7 of 15 def twice = { it * 2 } is called as twice() by mistake. Where does the error appear?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: Inside the body, as NullPointerException at it * 2, because it is null

    The implicit it is optional at the call, so a missing argument shows up later, where the body uses it. Name the parameter, or check it, when a missing value matters.

  8. Question 8 of 15 Which GDK method stops calling its closure at the first element for which the closure returns true, and returns true?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: any

    any answers whether one element matches, so the first true result settles it; every answers whether all match, so the first false result settles that.

  9. Question 9 of 15 What does this print?

    Read the code, then choose one answer.

    def r = [3, 4, 5].collect { n ->
        if (n > 3) return n * 10
    }
    println r
    Show the answer: Question 9 of 15

    Answer: [null, 40, 50]

    return gives the value of one call of the closure; collect builds its list from those values, one per element.

  10. Question 10 of 15 What happens to a script that writes break inside the closure of each?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: It does not compile, because break is only allowed inside loops or switches

    To stop early, choose a method that stops (find, any, every) or a for loop, where break and return work.

  11. Question 11 of 15 What is 'deploy'.&toUpperCase?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: A Closure that calls toUpperCase on the string deploy

    The method pointer operator .& turns a method into a closure, here bound to one object, so calling it with no arguments calls 'deploy'.toUpperCase().

  12. Question 12 of 15 In dynamic Groovy 4.0.33, what is upper after def upper = String::toUpperCase?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: A Closure, because :: is an alias for .& in dynamic Groovy

    In dynamic Groovy a method reference is a method pointer by another name, so it produces a Closure that collect can call for each element.

  13. Question 13 of 15 A Jenkinsfile helper written as def twice = (int n) -> n * 2 works in a Groovy 5.1.3 test. What happens in Jenkins 2.580.1?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: It does not compile, because the groovy-all 2.4.21 that Jenkins bundles has no lambda syntax

    Write { int n -> n * 2 } in code that Jenkins runs: closure literals work on every runtime of the track.

  14. Question 14 of 15 Since which Groovy version may a closure be assigned to a SAM type such as Predicate without writing as Predicate?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: 2.2.0

    Assigning a closure to a variable of a SAM type, or passing it where one is expected, converts it; as is only needed where the type is not known.

  15. Question 15 of 15 What does this print?

    Read the code, then choose one answer.

    Runnable job = { println 'build started' }
    println(job instanceof Runnable)
    job.run()
    Show the answer: Question 15 of 15

    Answer: true, then build started

    A SAM type has one abstract method; the closure becomes that method's body, so job.run() runs the closure.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

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 implicit it, and { -> … } takes none, so a stray argument fails at once with MissingMethodException.
  • return inside each ends one call of the closure, while find, any and every stop at the first result that settles the answer and a for loop’s return leaves 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, Function or Predicate when it is assigned to that type or passed where one is expected, but in a Jenkinsfile a closure given to a method such as toSorted gives 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.