Beginner

Control Flow in Groovy

Master conditionals, loops, and switch statements in Groovy with practical, Docker-ready examples covering if/else, ranges, the Elvis operator, and closure-based iteration

Introduction

Control flow is what gives a program its decision-making power—the ability to choose between paths, repeat work, and react to data. Groovy inherits the familiar control structures of Java (if/else, for, while, switch) but layers on dynamic, expressive features that make code shorter and more readable.

As a multi-paradigm language that blends object-oriented, functional, imperative, and scripting styles, Groovy gives you more than one way to express the same logic. You can write a classic C-style for loop, or you can iterate with a closure using each and times. You can write a verbose if/else chain, or collapse a null check into a single Elvis operator (?:). Groovy’s switch statement is also far more powerful than Java’s, matching on ranges, types, lists, and even regular expressions.

A key Groovy concept that shapes control flow is Groovy truth: any value can be evaluated as a boolean. Empty strings, empty collections, zero, and null are all “falsy,” while non-empty and non-zero values are “truthy.” This makes conditionals concise and idiomatic.

In this tutorial you’ll learn how to make decisions with conditionals, repeat work with loops, branch with the enhanced switch, and iterate the Groovy way with closures—all runnable in Docker without installing anything locally.

Conditionals: if, else, ternary, and Elvis

Groovy’s if/else works exactly like Java’s, but Groovy adds the ternary operator for short expressions and the Elvis operator (?:) for concise defaulting. The safe navigation operator (?.) lets you call methods on potentially-null objects without a NullPointerException.

Create a file named conditionals.groovy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Conditional control flow in Groovy

// if / else if / else
def temperature = 30
if (temperature > 35) {
    println "It's scorching hot!"
} else if (temperature >= 25) {
    println "It's a warm day."
} else {
    println "It's cool out."
}

// Ternary operator: condition ? ifTrue : ifFalse
def score = 72
def result = score >= 60 ? "Pass" : "Fail"
println "Result: ${result}"

// Elvis operator (?:) returns the left side if "truthy", else the right
// An empty string is "falsy" under Groovy truth, so we get the default
def username = ""
def displayName = username ?: "Anonymous"
println "User: ${displayName}"

// Safe navigation (?.) returns null instead of throwing on a null object
def user = null
println "Name length: ${user?.length()}"

Here temperature is 30, so the first condition fails but temperature >= 25 succeeds. The Elvis operator shines when defaulting: because an empty string is falsy in Groovy, username ?: "Anonymous" yields the fallback. The safe navigation operator returns null rather than crashing when user is null.

The Enhanced switch Statement

Groovy’s switch is one of its standout features. Unlike Java’s traditional switch, which only matched constants, Groovy matches using the isCase() method. This means a case can be a value, a range (1..9), a type (String), a list, or a regular expression pattern. Case order matters—the first match wins—so put more specific patterns first.

Create a file named switch_demo.groovy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Groovy's powerful switch statement

// Matching against ranges
def classify(value) {
    switch (value) {
        case 0:
            return "zero"
        case 1..9:
            return "single digit"
        case 10..99:
            return "double digit"
        default:
            return "large number"
    }
}

println "7 is a ${classify(7)}"
println "15 is a ${classify(15)}"
println "150 is a ${classify(150)}"

// Matching against a regex pattern, a type, and more
def describe(input) {
    switch (input) {
        case ~/\d{3}-\d{4}/:           // regex: must match fully
            return "a phone number"
        case String:                   // type check
            return "a String of length ${input.length()}"
        case Integer:
            return "an Integer worth ${input}"
        default:
            return "something else"
    }
}

println "hello is ${describe('hello')}"
println "42 is ${describe(42)}"
println "555-1234 is ${describe('555-1234')}"

The classify function uses ranges: 1..9 matches any single digit, 10..99 any two-digit number. The describe function shows off type matching (case String, case Integer) and regex matching (case ~/\d{3}-\d{4}/). Note that the regex case comes before case String—since '555-1234' is also a String, the order ensures the phone-number pattern wins.

Loops: for, while, and ranges

Groovy supports the classic C-style for loop, the for-in loop over ranges and collections, and the while loop. Ranges (5..1) are first-class objects and can even count downward.

Create a file named loops.groovy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Looping constructs in Groovy

// for-in over a descending range
print "Countdown: "
for (i in 5..1) {
    print "${i} "
}
println()

// for-in over a list
def languages = ["Groovy", "Java", "Kotlin"]
for (lang in languages) {
    println "JVM language: ${lang}"
}

// while loop computing a factorial
def n = 1
def factorial = 1
while (n <= 5) {
    factorial *= n
    n++
}
println "5! = ${factorial}"

// Classic C-style for loop
print "Powers of 2: "
for (int i = 1; i <= 16; i *= 2) {
    print "${i} "
}
println()

The range 5..1 counts down because Groovy detects the start is greater than the end. The for-in loop iterates cleanly over the languages list without index bookkeeping, while the while loop and the C-style for show the more traditional imperative forms still work.

Closure-Based Iteration and Loop Control

The most idiomatic Groovy way to iterate is with closures—small blocks of code passed to methods like each, eachWithIndex, and times. These read naturally and avoid off-by-one errors. For traditional loops, break exits early and continue skips to the next iteration.

Create a file named iteration.groovy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Closure-based iteration and loop control

// times: run a closure a fixed number of times (0-indexed)
print "Squares: "
4.times { i ->
    print "${i * i} "
}
println()

// each: iterate over every element of a collection
[10, 20, 30].each { num ->
    println "Value: ${num}"
}

// eachWithIndex: get both the element and its index
["a", "b", "c"].eachWithIndex { item, idx ->
    println "${idx}: ${item}"
}

// continue: skip odd numbers
print "Evens under 10: "
for (i in 0..9) {
    if (i % 2 != 0) continue
    print "${i} "
}
println()

// break: stop at the first match
print "First multiple of 7: "
for (i in 1..100) {
    if (i % 7 == 0) {
        println i
        break
    }
}

4.times runs the closure with i from 0 to 3. each and eachWithIndex iterate over collections with a closure parameter. Note that break and continue work in the classic for/while loops but not inside each/times closures—to exit a closure-based iteration early, you return from the closure (which skips to the next element) or use a different method like find.

Running with Docker

Docker lets you run all four examples consistently without installing Groovy locally.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the official image
docker pull groovy:4.0-jdk17-alpine

# Run the conditionals example
docker run --rm -v $(pwd):/app -w /app groovy:4.0-jdk17-alpine groovy conditionals.groovy

# Run the switch example
docker run --rm -v $(pwd):/app -w /app groovy:4.0-jdk17-alpine groovy switch_demo.groovy

# Run the loops example
docker run --rm -v $(pwd):/app -w /app groovy:4.0-jdk17-alpine groovy loops.groovy

# Run the iteration example
docker run --rm -v $(pwd):/app -w /app groovy:4.0-jdk17-alpine groovy iteration.groovy

On Windows PowerShell, replace $(pwd) with ${PWD}.

Expected Output

Running conditionals.groovy:

It's a warm day.
Result: Pass
User: Anonymous
Name length: null

Running switch_demo.groovy:

7 is a single digit
15 is a double digit
150 is a large number
hello is a String of length 5
42 is an Integer worth 42
555-1234 is a phone number

Running loops.groovy:

Countdown: 5 4 3 2 1 
JVM language: Groovy
JVM language: Java
JVM language: Kotlin
5! = 120
Powers of 2: 1 2 4 8 16 

Running iteration.groovy:

Squares: 0 1 4 9 
Value: 10
Value: 20
Value: 30
0: a
1: b
2: c
Evens under 10: 0 2 4 6 8 
First multiple of 7: 7

Key Concepts

  • Groovy truth: Any value can be evaluated as a boolean. Empty strings, empty collections, zero, and null are falsy; everything else is truthy. This powers concise conditionals and the Elvis operator.
  • Elvis operator (?:): A shorthand for “use this value, or a default if it’s falsy”—name ?: "Anonymous" replaces a verbose if/else.
  • Safe navigation (?.): Calls a method only if the object is non-null, returning null instead of throwing a NullPointerException.
  • Powerful switch: Groovy’s switch matches on ranges, types, lists, and regex patterns via isCase(), not just constants. Order your cases from most specific to least specific.
  • First-class ranges: Expressions like 1..9 and 5..1 are real objects you can loop over, match against, or count downward with.
  • Closure-based iteration: each, eachWithIndex, and times are the idiomatic Groovy way to iterate, reading more naturally than index-based loops.
  • break/continue caveat: These work in for and while loops but not inside each/times closures—use return to skip an element in a closure instead.

Running Today

All examples can be run using Docker:

docker pull groovy:4.0-jdk17-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining