Control Structures
|
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). Groovy’s control structures look like Java’s, but three things change how they behave: any object
can be a condition (the Groovy truth), switch is
driven by a method called isCase instead of equality, and the loop and instanceof syntax has been extended over the
releases (switch expressions in 4.0, the for index variable in 5.0, the switch position rule, labelled blocks and instanceof pattern scoping in 6.0).
For the Java baseline see Java Control Flow.
if / else and conditional expressions
if, else if and else work as in Java, with one difference: the condition does not have to be a boolean. Groovy
applies the Groovy truth, so null, an empty string, an empty collection and 0 are false. The ternary operator and the
Elvis operator are expressions built on the same rule.
def moisture = 42
if (moisture < 20) {
println 'water now'
} else if (moisture < 60) {
println 'ok'
} else {
println 'too wet'
}
def zone = null
if (zone) println 'has zone' else println 'no zone (null is false)'
println(moisture > 30 ? 'fine' : 'dry')
ok
no zone (null is false)
fine
switch and isCase
A Groovy switch does not compare with ==. For each case, it evaluates caseValue.isCase(switchValue) and runs the
branch when that returns true (through the Groovy truth). The GDK supplies an isCase for the common types, which is why
one switch can mix very different kinds of labels. The switch value can be any type, and the labels need not even be
constants.
The flowchart shows the matching order; the example after it exercises every kind of label.
The cases are tried in source order and the first match wins, so put specific labels before general ones: in the example
below, Integer has to come after the range and the list or it would swallow them.
def classify(x) {
switch (x) {
case null: return 'null'
case 'frost': return 'the word frost'
case ~/temp-\d+/: return 'regex match'
case 0..9: return 'single digit'
case [10, 20, 30]: return 'a round number'
case Integer: return 'some other int'
case { it instanceof Number && it > 1000 }: return 'huge number'
case List: return 'a list'
default: return 'unknown'
}
}
[null, 'frost', 'temp-21', 7, 20, 99, 5000.5, [1], 'xyz'].each { println "${it.inspect()} -> ${classify(it)}" }
null -> null
'frost' -> the word frost
'temp-21' -> regex match
7 -> single digit
20 -> a round number
99 -> some other int
5000.5 -> huge number
[1] -> a list
'xyz' -> unknown
The label types and what their isCase means:
| Label | Matches when |
|---|---|
A class ( |
The value is an instance of the class ( |
A range ( |
The range contains the value |
A regex pattern ( |
The pattern matches the whole string ( |
A collection ( |
The collection contains the value |
A closure |
The closure, called with the value, returns a true result |
|
The value is |
Any other object |
|
Because dispatch is just a method call, you can make your own types usable as labels by defining isCase, and you can call
the method directly outside a switch. Control flow is the classic C-style one: a matching branch falls through into the
next unless it ends with break, return or throw.
// fall-through
def fall(n) {
def out = []
switch (n) {
case 1: out << 'one'
case 2: out << 'two'; break
case 3: out << 'three'
}
out
}
println fall(1); println fall(2); println fall(3)
// a custom isCase
class Band { int lo, hi; boolean isCase(v) { v >= lo && v <= hi } }
def cold = new Band(lo: -50, hi: 5)
switch (3) { case cold: println 'cold band'; break; default: println 'other' }
// isCase is an ordinary method
println(cold.isCase(3))
println('abc'.isCase('abc'))
println(Integer.isCase(1))
println((1..5).isCase(3))
println([1, 2].isCase(2))
[one, two]
[two]
[three]
cold band
true
true
true
true
true
Source: Groovy GDK: Object.isCase
A switch used for classification (by type, range or pattern) is where this pays off most. See
Regular Expressions for regex labels and
Ranges for range labels. Closure labels are covered in
Closures.
switch expressions
Since Groovy 4.0 a switch can be used as an expression that produces a value, with the arrow form
case label -> result. An arrow branch never falls through, so no break is needed. Several labels can share a branch
(case 'S', 'M' ->), a branch can be a block, and a block produces its value with yield. The colon form can also be
an expression, in which case each branch ends with yield. The labels are still matched through isCase, so ranges,
closures and classes work as before.
def label(int t) {
def r = switch (t) {
case 0..4 -> 'frost risk'
case 5..25 -> 'comfortable'
case { it > 25 } -> 'hot'
default -> 'below zero'
}
r
}
println label(2); println label(12); println label(31); println label(-3)
def size = 'M'
def liters = switch (size) {
case 'S', 'M' -> 5
case 'L' -> {
def base = 8
yield base * 2
}
default -> 0
}
println liters
def old = switch (size) {
case 'S':
case 'M':
yield 5
default:
yield 0
}
println old
frost risk
comfortable
hot
below zero
5
5
The position decides (Groovy 6.0)
Groovy 6.0 adopts Java’s rule that the arrow decides fall-through while the position decides whether the switch
produces a value. A switch used as a statement, even with arrows, therefore does not have to cover every value. A switch
used as an expression must, and a value that no branch handles fails at run time. Statically compiled switch statements
also gain tableswitch/lookupswitch dispatch, and a duplicate label is diagnosed for statements as it already was for
expressions.
def run(x) {
// statement position: an arrow switch need not be exhaustive
switch (x) {
case 1 -> println 'one'
case 2 -> println 'two'
}
println "done $x"
}
run(1); run(9)
try {
def v = switch (9) { case 1 -> 'one' } // expression position: must cover 9
println v
} catch (Throwable e) {
println "${e.class.name}: ${e.message}"
}
one
done 1
done 9
java.lang.IllegalStateException: the switch expression does not cover the value 9
Source: Groovy 6.0 release notes, switch
When writing an expression switch, add a default branch unless the labels are provably complete. Code written for
Groovy 5 or earlier that relied on the old behaviour of arrow switch statements should be re-run on 6.0.
Loops
The classic for loop
The C-style for accepts several variables and several update expressions, separated by commas, in addition to the
single-variable form.
for (int i = 0; i < 3; i++) print "$i "
println()
for (int i = 0, j = 10; i < j; i += 4, j -= 1) println "i=$i j=$j"
0 1 2
i=0 j=10
i=4 j=9
for in
for (x in iterable) iterates over anything Groovy can iterate: ranges, lists, arrays, maps (as entries), strings (as
characters) and any object with an iterator(). Java’s for (x : iterable) form is also accepted. Since Groovy 5.0 an
extra leading variable receives the zero-based index, with or without types.
for (z in ['north', 'south']) println z
for (c in 'abc') print c.toUpperCase()
println()
for (e in [a: 1, b: 2]) println "${e.key}=${e.value}"
// Groovy 5.0: index variable first
for (i, z in ['north', 'south']) println "$i: $z"
for (int i, String z in ['x', 'y']) println "$i/$z"
north
south
ABC
a=1
b=2
0: north
1: south
0/x
1/y
while and do / while
while is unchanged. do/while has been available since Groovy 3.0 (the Parrot parser); the older parser did not
support it.
int n = 3
while (n > 0) { print n-- ; print ' ' }
println()
int k = 0
do { k++ } while (k < 5)
println k
3 2 1
5
Groovy 6.0 also gives each iteration of a for in loop its own binding of the loop variable when a closure, lambda or
anonymous inner class captures it, so deferred code sees the value of the iteration that created it, as in Java. The old
single shared binding can be restored with the system property groovy.forin.per.iteration.capture=false.
Idiomatic Groovy often replaces loops with closures such as each, eachWithIndex, times or (1..3).each { }, see
Closures and
Working with Collections. Note that
break and continue do not apply to those closure-based iterations (see below).
break, continue and labels
break leaves the innermost loop or switch; continue skips to the next iteration. A label (name:) before a
statement lets break label and continue label target an outer loop. Since Groovy 6.0 a label can also precede a plain
block, and break label then leaves that block, which gives a structured early exit without a helper method.
outer:
for (i in 1..3) {
for (j in 1..3) {
if (j == 2) continue
if (i == 2) continue outer
if (i == 3) break outer
println "i=$i j=$j"
}
}
println 'after'
// Groovy 6.0: labelled break out of a plain block
found: {
for (x in [4, 8, 15]) if (x > 5) { println "first > 5: $x"; break found }
println 'none'
}
for (i in 1..5) { if (i % 2) continue; println i; if (i > 3) break }
// inside a closure, return only ends this call of the closure
[1, 2, 3].each { if (it == 2) return; println it }
i=1 j=1
i=1 j=3
after
first > 5: 8
2
4
1
3
The last line is a common trap: inside the closure of each, return behaves like continue (it ends only that call),
and break is not available at all. To stop early use a real for loop, find, any or takeWhile, or throw.
instanceof and pattern variables
Groovy accepts Java’s instanceof type pattern (JEP 394): x instanceof String s tests and binds s in one step. Since
Groovy 6.0 the scope of the pattern variable follows Java’s flow rules, and the negated form !instanceof introduces a
binding too. The variable is in scope only where the test is known to be true: in the then branch for instanceof, in
the else branch for !instanceof, and on the right-hand side of &&. Elsewhere, dynamic code fails with a
MissingPropertyException and statically checked code with a compile error. This is a breaking change from earlier
releases, where the variable was visible more widely in dynamic code.
Object o = 'greenhouse'
if (o instanceof String s && s.size() > 5) println s.toUpperCase()
if (o !instanceof Integer) println 'not an int'
if (!(o instanceof Integer i)) println 'neg form'
Object p = 42
if (p !instanceof Integer n) { println 'no' } else { println n + 1 }
GREENHOUSE
not an int
neg form
43
Compared with Java, Groovy’s dynamic typing makes the cast less necessary, because a method call is resolved on the runtime
type anyway; the pattern variable matters mostly under static type checking,
where it replaces the cast. For Java’s own pattern matching, including records and switch patterns, see
Java Pattern Matching.
Where Groovy 6.0 and older books differ
Books written for Groovy 1.x and 2.x, including the first edition of Groovy in Action (Groovy 1.0), describe a switch
that only has the colon form and cannot produce a value, loops without the index variable, no do/while, and no
instanceof patterns. All of the following are later additions: the do/while loop and the !instanceof operator (3.0),
switch expressions (4.0), instanceof patterns and the for index variable (5.0), and the position rule, labelled blocks,
binding a pattern variable with !instanceof and the flow-scoped pattern variable (6.0). The isCase dispatch, fall-through and the Groovy truth
described above have not changed.