Closures

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Groovy 6.0 (6.0.0, 2026-09-22) and every example was run with Groovy 6.0.0 on JDK 26 (checked on 2026-10-14). A closure is an anonymous block of code that is an object: it can be stored in a variable, passed to a method, returned from one, and called later, and it keeps access to the variables that were in scope where it was written. Closures are what make the GDK iteration methods (each, collect, findAll), builders and DSLs read the way they do. This page covers the closure itself. How a closure resolves this, owner and delegate is on Closure Delegation: Owner, Delegate & This, and currying, memoization and composition are on Functional Programming. Java’s counterpart is described on Java Lambdas & Method References, and Kotlin’s on Kotlin Lambdas & Higher-Order Functions.

Defining and calling a closure

A closure literal is a pair of braces, optionally containing a parameter list followed by ->, then statements. The value of the literal is an instance of groovy.lang.Closure; the compiler generates a subclass for each literal.

def square = { int x -> x * x }

println square.class.superclass.name
println(square instanceof Closure)
println square(4)            // implicit call()
println square.call(5)       // explicit call
groovy.lang.Closure
true
16
25

Calling square(4) is shorthand for square.call(4); both end up in the generated doCall method. Because a closure is a value, a local variable that holds one can be invoked like a method, which a plain def method cannot be passed around as without a method reference (see Method references and closures).

Parameters

Groovy accepts four shapes of parameter list, and the differences matter more than in Java because the compiler does not force you to state them.

def noArgs   = { -> 'none' }          // explicitly no parameters
def implicit = { it * 2 }             // no arrow: one implicit parameter named it
def explicit = { x -> x * 2 }         // one named parameter
def empty    = { }                    // no arrow, so it still has the implicit parameter

println noArgs()
println implicit(21)
println explicit(4)
println empty()
println noArgs.maximumNumberOfParameters
println implicit.maximumNumberOfParameters
println implicit.parameterTypes*.simpleName
def itNoArg = { it }
println itNoArg()                     // it is null when no argument is passed
try { noArgs(1) } catch (e) { println e.class.simpleName }
none
42
8
null
0
1
[Object]
null
MissingMethodException
  • A closure written without an arrow has exactly one implicit parameter, it. Call it with no argument and it is null; call it with several and you get a MissingMethodException, which is also what noArgs(1) does above.

  • { -> ... } declares that there are no parameters, so it is not available and any argument is rejected. Use it for callbacks that take nothing, such as Runnable and Supplier.

  • Parameters may be typed (int x) or untyped (x). An untyped parameter is Object. A typed closure is selected by dispatch on the argument type, so passing the wrong type fails at call time, not at compile time (unless @CompileStatic or @TypeChecked is on, see Static Type Checking & @CompileStatic).

  • it is a convention of the single-parameter form, not a keyword. In nested closures an inner it hides the outer one, so name the parameters once the nesting gets deep.

Several parameters and destructuring

When a closure declares two or more parameters and is handed a single List or Map.Entry, the GDK methods spread it. That is why Map.each and collect over a list of pairs work with two parameters.

def m = [a: 1, b: 2]
m.each { k, v -> print "$k=$v " }
println()
m.each { e -> print e.class.simpleName + ' ' }    // one parameter: the Map.Entry itself
println()
m.each { println it }
println([[1, 2], [3, 4]].collect { a, b -> a * b })
println([[1, 2], [3, 4]].collect { it.sum() })
def two = { a, b -> a + b }
println two(*[3, 4])                             // spread a list into the arguments
a=1 b=2
Entry Entry
a=1
b=2
[2, 12]
[3, 7]
7

The spreading is done by the iteration method (it passes the elements as separate arguments), not by the closure, so calling two([3, 4]) yourself does not unpack the list. Use the spread operator * as shown.

Default values and varargs

A parameter can have a default value; the compiler generates the shorter overloads of doCall. The last parameter can be an array, which makes it a varargs parameter.

def sum = { a, b = 10 -> a + b }
println sum(1)
println sum(1, 2)
println sum.maximumNumberOfParameters

def varargs = { String label, Object... values -> "$label=${values.sum()}" }
println varargs('t', 1, 2, 3)

def varargs2 = { int... n -> n.length }
println varargs2()
println varargs2(1, 2)

def typed = { String s, int n -> s * n }
println typed('ab', 3)
println typed.parameterTypes*.simpleName
11
3
2
t=6
0
2
ababab
[String, int]

maximumNumberOfParameters reports the longest signature (2 for sum), while getParameterTypes() shows the declared types of that signature. Both are useful when a library inspects a closure to decide how to call it, which is how the iteration methods tell a { k, v -> ... } closure from a { e -> ... } one.

Return values and return

A closure returns the value of its last evaluated expression, exactly like a Groovy method; return is allowed but usually omitted. A closure whose last statement is an if without else returns the value of the branch that ran, or null.

What surprises Java programmers is where return goes. It leaves the closure, never the surrounding method.

def last = { x -> if (x > 0) 'positive' else 'other' }
println last(1)
def early = { x -> if (x < 0) return 'negative'; 'non-negative' }
println early(-1)
println early(1)

def found = []
[1, 2, 3, 4].each { if (it == 2) return; found << it }   // return = continue
println found

def firstEven(list) { for (n in list) if (n % 2 == 0) return n; null }
println firstEven([1, 3, 4, 6])
positive
negative
non-negative
[1, 3, 4]
4

Inside each, return skips to the next element, so it plays the role of continue; there is no break. To stop early, use a method with a for loop as firstEven does, or a GDK method that stops by itself, such as find, any, every or takeWhile. Throwing an exception also leaves every closure, at the cost of using control flow for exceptions. See Control Structures for the loop forms.

Captured variables

A closure sees the local variables, parameters and properties of the code around it. Locals are captured by reference: the closure reads and writes the same variable, even after the enclosing method has returned. This is what Java’s lambdas forbid (they need effectively final variables), and it makes a counter factory a three-line closure.

def counter = 0
def inc = { counter++ }
3.times { inc() }
println counter

def makeCounter = { int n = 0 -> { -> ++n } }
def c1 = makeCounter()
def c2 = makeCounter(10)
println([c1(), c1(), c2()])

def adder = { int n -> { int x -> x + n } }
println adder(5)(10)
3
[1, 2, 11]
15

Each call to makeCounter creates a fresh n, so c1 and c2 keep independent state. The cost is that shared mutable state captured by a closure that runs on another thread is not thread safe; see Parallel Collections & Dataflow.

Closures as arguments

When the last parameter of a method is a Closure, the closure can be written after the closing parenthesis, and the parentheses can be dropped entirely if there is no other argument. This is the syntax that every Groovy builder and DSL is made of.

def twice(int n, Closure body) { n.times(body) }
twice(2) { println "run $it" }

def apply(Closure c) { c(5) }
println apply { it + 1 }
run 0
run 1
6

The trailing form has a pitfall. A brace at the start of a statement is an ordinary code block and runs at once, but a brace on the line after an expression is read as a closure argument to that expression, because the newline does not end the statement there. A semicolon ends it explicitly:

{ println 'top' }             // a block at the start of a statement: it runs

def x = 1;                    // the semicolon ends the statement
{ println 'after semicolon' }

try {
    def y = 1
    { println 'never' }       // parsed as 1({ ... }), i.e. 1.call({ ... })
} catch (MissingMethodException e) {
    println e.message.take(57)
}
top
after semicolon
No signature of method: call for class: java.lang.Integer

A literal empty map is [:], because { } would be a block or a closure.

Closures and lambdas

Groovy accepts Java’s lambda syntax, but in dynamic code the result is still a closure. The arrow form does not create a Java lambda object unless the compiler is told to be static and the target type is a functional interface.

def lam = (int a, int b) -> a + b
println lam.getClass().interfaces*.simpleName
println lam(1, 2)
def lam2 = x2 -> x2 * 3
println lam2(3)
println lam instanceof Closure
[GeneratedClosure]
3
9
true

With @CompileStatic the compiler honours the target type. A lambda assigned to a functional interface becomes a real JDK lambda, while one assigned to def or Closure is still a closure:

import groovy.transform.CompileStatic
import java.util.function.IntBinaryOperator

@CompileStatic
class Calc {
    static IntBinaryOperator lam() { return (int a, int b) -> a + b }
    static IntBinaryOperator coerced() { return { int a, int b -> a + b } as IntBinaryOperator }
    static void run() {
        Closure<Integer> c = { int a, int b -> a + b }
        println c.getClass().superclass.simpleName
        def l = (int a, int b) -> a + b
        println l.getClass().interfaces*.simpleName
        println lam().getClass().interfaces*.simpleName
        println lam().applyAsInt(2, 3)
        println coerced().getClass().isSynthetic()
        println lam().getClass().isSynthetic()
    }
}
Calc.run()
Closure
[GeneratedClosure]
[IntBinaryOperator]
5
false
true

The practical differences are these:

Aspect Closure Java lambda (@CompileStatic and a functional interface target)

Type

groovy.lang.Closure subclass

JDK-generated synthetic class implementing the interface

Free variables

Captured by reference, may be reassigned

Effectively final, as in Java

this, owner, delegate

Available and reassignable

Not available; this is the enclosing instance

Extra API

curry, memoize, >>, <<, trampoline, strategies

Only what the interface defines

Coercion to SAM

Automatic when the target type is a SAM, or with as

Already the target type

Use plain closures in dynamic Groovy; use lambdas when a statically compiled class must hand a Java API a functional interface without a Closure in between, or when a closure’s delegate machinery is not wanted. See Differences with Java for how Groovy treats other Java constructs.

Closures in GStrings

A placeholder ${x} is evaluated when the GString is created. To postpone the evaluation, use a closure placeholder ${-> x}, which is evaluated every time the GString is converted to a String.

def x = 1
def eager = "x=$x"
def lazy  = "x=${-> x}"
x = 2
println eager
println lazy
x=1
x=2

The rule and its pitfalls for map keys and equals are on Strings & GStrings.

Coercion to a SAM type

A closure converts to any interface or class with a single abstract method. The conversion is automatic when the assignment or argument target is such a type, and explicit with as otherwise. That is what lets a Groovy closure be passed to Java APIs that expect Runnable, Comparator, Callable or java.util.function.*.

Runnable r = { println 'running' }
r.run()

java.util.function.Function<Integer, Integer> f = { it + 1 }
println f.apply(1)

def cmp = { a, b -> b <=> a } as Comparator
println cmp.class.interfaces*.simpleName

interface Greeter { String greet(String n) }
Greeter g = { "hi $it" }
println g.greet('bob')

abstract class Shape {
    abstract double area()
    String toString() { "area=${area()}" }
}
Shape s = { 2.5d } as Shape
println s
running
2
[Comparator]
hi bob
area=2.5

Coercion applies to an abstract class with one abstract method too (Shape above). For an interface with several methods, a map of closures keyed by method name coerces to it:

def m = [compare: { a, b -> a <=> b }] as Comparator
println m.compare(1, 2)
-1

A pitfall comes from the Java collections API. List.sort(Comparator) is the JDK method: it sorts in place and returns nothing, so its result is null, while sort(Closure) is the GDK method and returns the list.

def list = [3, 1, 2]
def r1 = list.sort(new Comparator<Integer>() { int compare(Integer a, Integer b) { b <=> a } })
println r1
def r2 = [3, 1, 2].sort { a, b -> b <=> a }
println r2
null
[3, 2, 1]

Use the closure overload, or call the sort for its side effect and then read the list. The sort family is covered on Working with Collections.

Method references and closures

The method pointer operator .& turns an existing method into a closure, and Type.&method does the same for an instance method of a type. The result is a MethodClosure, which can be passed wherever a closure is expected.

def up = 'abc'.&toUpperCase
println up.class.simpleName + ' ' + up()
println(['a', 'b'].collect(String.&toUpperCase))
MethodClosure ABC
[A, B]

The operator is described with the other object operators on Object Operators, and the Java equivalent Type::method on the Java page linked above.

Where the book is outdated

The Manning book Groovy in Action (publisher page in the references) introduces closures at length, and its picture of what a closure is still holds. Check these points against the current documentation instead:

  • Lambda syntax and the @CompileStatic behaviour shown above did not exist when the book was written; closures were the only anonymous-function form.

  • The conversion rules for SAM types, and the allocation-free GDK overloads that accept java.util.function interfaces (Groovy 6.0), are newer than the text. See Functional Programming.

  • Version and JDK baselines: Groovy 6.0 needs JDK 17 or later.