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 2 – Core syntax: values, strings, operators and control flow

Groovy truth, null safety and the Elvis operator

What if (value) decides in Groovy, why 'false' and File objects surprise, and null-safe code with ?., ?:, ?= and ?[], with the runtimes that accept each.

  • 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

  • Predict how null, numbers, strings, collections, matchers and files convert to boolean on each runtime
  • Implement asBoolean() so that a class of your own has the truth you choose
  • Convert string flags and numbers with toBoolean(), isInteger() and toInteger() instead of relying on truth
  • Write null-safe code with ?., ?:, ?= and ?[], and state which runtimes accept each

Before you start

What you’ll learn · Before you start

What you will learn

  • Predict how null, numbers, strings, collections, matchers and files convert to boolean on each runtime
  • Implement asBoolean() so that a class of your own has the truth you choose
  • Convert string flags and numbers with toBoolean(), isInteger() and toInteger() instead of relying on truth
  • Write null-safe code with ?., ?:, ?= and ?[], and state which runtimes accept each

Before you start

On this page

if ('false') runs its block in Groovy, because a non-empty String is true, and retries ?: 3 turns a deliberate retries = 0 into 3. Both follow from one rule set, the Groovy truth: every value, not only a boolean, can decide an if, and the rules for numbers, text, collections and files are not Java’s. Groovy 5 even changed the truth of a File, so the same if (report) decides differently on Jenkins’ Groovy 2.4.21 and on Groovy 6.0.0.

You will predict the truth of any value on each runtime, give a class of your own a truth of its choosing, read 'false' and '0' from the environment correctly, and use ?., ?:, ?= and ?[] where each is safe.

Empty, zero and null are false; everything else is true

The documentation lists the rules of the Groovy truth: Groovy's rules for turning any value into true or false in a condition: null, zero and empty strings, collections, maps and arrays are false, and every other value is true unless its class defines otherwise. type by type: null is false, a number is false when it is zero, and a String, a collection, a map or an array is false when it is empty. A regular-expression matcher is true when it finds a match, and any other object is true because it is not null. Each rule is an asBoolean(): The method that Groovy calls to decide the truth of a value in if, while, ! and ?:. The Groovy Development Kit defines it for strings, numbers, collections, maps, matchers and, from Groovy 5, files; a class can define its own. method that the Groovy Development Kit gives a type. Step through six values and watch which question decides each one:

Interactive · stage by stage How if (value) decides true or false Step through six values of the table. Watch which question settles each one.
How if (value) decides true or false, step 6 of 6. Showing Is it null? (no), Is it a Boolean? (no), An asBoolean() method? (File: none before Groovy 5), Any other object (yes: true). In focus: Any other object.Is it null?noIs it a Boolean?noAn asBoolean() method?File: none before Groovy 5Any other objectyes: true

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

Every step as text
  1. null. The first question is whether the value is null. null is false, as the first line of the table shows, and no method is called.
  2. 'false'. 'false' is not null and not a Boolean. The GDK gives every CharSequence an asBoolean() that is false only for length 0, so the five letters of 'false' make it true.
  3. [0]. A list's asBoolean() asks only whether it is empty. [0] holds one item, so it is true, whatever the item is; [] is false.
  4. 0.0. A number is false when its double value is 0, the GDK says, so 0 and 0.0 are false and -1 is true.
  5. A missing File. From Groovy 5 the GDK gives File an asBoolean() that asks whether the file exists, so new File('nope.txt') is false on 5.1.3 and 6.0.0.
  6. The same File, older. Groovy 4.0.33 and 2.4.21 have no such method for File. The value falls through to the last question, a non-null object, and is true.

The program prints the truth of sixteen values on Groovy 6.0.0. The session then runs it on each older runtime, and diff prints only the lines that changed:

truth.groovy GROOVY · truth.groovy
// The Groovy truth of common values: what if (value) decides for each one.
def values = [
    'null'                 : null,
    '0'                    : 0,
    '0.0'                  : 0.0,
    '-1'                   : -1,
    "''"                   : '',
    "' '"                  : ' ',
    "'false'"              : 'false',
    "'0'"                  : '0',
    '[]'                   : [],
    '[0]'                  : [0],
    '[:]'                  : [:],
    'new int[0]'           : new int[0],
    "'a' =~ /b/"           : ('a' =~ /b/),
    "'a' =~ /a/"           : ('a' =~ /a/),
    'Double.NaN'           : Double.NaN,
    "new File('nope.txt')" : new File('nope.txt'),
]
values.each { code, value -> println "${code.padRight(22)}${value ? 'true' : 'false'}" }
The truth of sixteen values, on four runtimes Python · truth_session.py
from session import jenkins_jar, run, use

# The truth of sixteen values on Groovy 6.0.0, then on each older runtime: diff prints only the lines that differ from 6.0.0's.
run("""
groovy truth.groovy | tee on-6.0.0.txt
""")
for version in ("5.1.3", "4.0.33"):
    use(version)
    run(f"""
    groovy truth.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 truth.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 truth.groovy | tee on-6.0.0.txt
null                  false
0                     false
0.0                   false
-1                    true
''                    false
' '                   true
'false'               true
'0'                   true
[]                    false
[0]                   true
[:]                   false
new int[0]            false
'a' =~ /b/            false
'a' =~ /a/            true
Double.NaN            false
new File('nope.txt')  false
# The groovy command is Groovy 5.1.3 from here on.
$ groovy truth.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 truth.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'
16c16
< new File('nope.txt')  false
---
> new File('nope.txt')  true
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 truth.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'
15,16c15,16
< Double.NaN            false
< new File('nope.txt')  false
---
> Double.NaN            true
> new File('nope.txt')  true
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 truth_session.py

Fourteen lines were the same everywhere. 'false' and '0' are true, because they are non-empty text, and [0] is true, because the list is not empty, whatever it holds. Two lines changed. A missing File is false on 6.0.0 and 5.1.3 but true on 4.0.33 and 2.4.21, and Double.NaN is false on 4.0.33 and later but true on Jenkins’ 2.4.21. The GDK’s rule for numbers is false only for zero, which makes NaN true; it lists a separate truth for Double, false for zero and for NaN, only since Groovy 2.6.0. Code that tests a computed double with if can therefore take different paths on the two runtimes.

Key idea

null, zero and empty are false, and every other value is true, including the text 'false', the text '0' and the list [0].

asBoolean() gives a class its truth, and File’s changed in Groovy 5

A class decides its own truth by defining boolean asBoolean(), as the documentation shows. A basket can then be false while it is empty, without callers having to know about its list. The second program tests a File before and after it exists; the Groovy 5 release notes say that a File or Path “now coerce to a boolean based on whether the file or path exists”. Before Groovy 5, any non-null File was true:

basket.groovy and fileTruth.groovy

GROOVY (basket.groovy) · basket.groovy

class Basket {
    List<String> items = []
    boolean asBoolean() { !items.isEmpty() }
}

def basket = new Basket()
println "an empty basket: ${basket ? 'true' : 'false'}"
basket.items << 'tea' << 'cake'
println "two items:       ${basket ? 'true' : 'false'}"
println "!basket:         ${!basket}"

GROOVY (fileTruth.groovy) · fileTruth.groovy

def report = new File('report.txt')
println "before writing: ${report ? 'true' : 'false'}"
report.text = 'done'
println "after writing:  ${report ? 'true' : 'false'}"
asBoolean() of your own, and a File's truth, on four runtimes Python · custom_session.py
from session import jenkins_jar, run, use

# A class with asBoolean(), then a File before and after it exists, on the four runtimes.
run("""
groovy basket.groovy
rm -f report.txt
groovy fileTruth.groovy
""")
for version in ("5.1.3", "4.0.33"):
    use(version)
    run("""
    rm -f report.txt
    groovy fileTruth.groovy
    """)
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain basket.groovy
rm -f report.txt
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain fileTruth.groovy
""")

Output

$ groovy basket.groovy
an empty basket: false
two items:       true
!basket:         false
$ rm -f report.txt
$ groovy fileTruth.groovy
before writing: false
after writing:  true
# The groovy command is Groovy 5.1.3 from here on.
$ rm -f report.txt
$ groovy fileTruth.groovy
before writing: false
after writing:  true
# The groovy command is Groovy 4.0.33 from here on.
$ rm -f report.txt
$ groovy fileTruth.groovy
before writing: true
after writing:  true
# 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 basket.groovy
an empty basket: false
two items:       true
!basket:         false
$ rm -f report.txt
$ java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain fileTruth.groovy
before writing: true
after writing:  true

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 custom_session.py

The empty basket was false, the basket with two items true, and !basket called the same method. For the file, 6.0.0 and 5.1.3 printed false before it was written and true after, while 4.0.33 and 2.4.21 printed true both times: there, if (report) only says that the variable is not null. The GDK’s documentation of File.asBoolean() names the way back for code that is not ready: the system property groovy.truth.file.exists.enabled set to false.

Testing that a File variable is set with if (file)

On Groovy 5 and 6, if (output) asks whether the file exists, so a step that creates the file later is skipped. Write if (output != null) for “the variable is set” and if (output.exists()) for “the file is there”; both mean the same on every runtime.

Checkpoint

Check: The Groovy truth of common values · asBoolean() and the truth of a File

  1. Question 1 of 2 Which of these values are true in an if? Choose every one that is.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • [null]
    • '0'

    Empty and zero are false, everything else is true: '0' and [null] are true, 0.0 and [:] are false, the same rules that made 'false' and [0] true in the session.

  2. Question 2 of 2 def out = new File('build.log'), and the file does not exist yet. Where does if (out) { … } run its block?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: On Groovy 4.0.33 and 2.4.21, but not on 5.1.3 and 6.0.0

    The Groovy 5 release notes made File and Path true only when they exist. Write out != null or out.exists() to say which question you mean on every runtime.

Text that says false is still true

Environment variables and command-line arguments always arrive as text, and so does a setting read from a properties file or a form. The text 'false' is five characters long, so it is true. To read text as a flag or a number, convert it first: the GDK’s toBoolean() returns true when the trimmed text is "true", "y" or "1", ignoring case, and false for anything else, and isInteger() says whether toInteger() will work:

flags.groovy GROOVY · flags.groovy
def skip = 'false'
println "if ('false')           ${skip ? 'runs' : 'does not run'}"
['true', 'TRUE', 'y', '1', 'yes', 'false', '0', ''].each {
    println "'${it}'.toBoolean()".padRight(23) + it.toBoolean()
}
println "'12'.isInteger()       ${'12'.isInteger()}"
println "'1.5'.isInteger()      ${'1.5'.isInteger()}"
println "'10' > '9'             ${'10' > '9'}"
println "'10'.toInteger() > 9   ${'10'.toInteger() > 9}"
try {
    println "'10' > 9               ${'10' > 9}"
} catch (e) {
    println "'10' > 9               ${e.getClass().name}"
}
String flags and comparisons, on four runtimes Python · flags_session.py
from session import jenkins_jar, run, use

# String flags and comparisons on Groovy 6.0.0, then on each older runtime: diff prints only the lines that differ from 6.0.0's.
run("""
groovy flags.groovy | tee on-6.0.0.txt
""")
for version in ("5.1.3", "4.0.33"):
    use(version)
    run(f"""
    groovy flags.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 flags.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 flags.groovy | tee on-6.0.0.txt
if ('false')           runs
'true'.toBoolean()     true
'TRUE'.toBoolean()     true
'y'.toBoolean()        true
'1'.toBoolean()        true
'yes'.toBoolean()      false
'false'.toBoolean()    false
'0'.toBoolean()        false
''.toBoolean()         false
'12'.isInteger()       true
'1.5'.isInteger()      false
'10' > '9'             false
'10'.toInteger() > 9   true
'10' > 9               java.lang.IllegalArgumentException
# The groovy command is Groovy 5.1.3 from here on.
$ groovy flags.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 flags.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 flags.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'
14c14
< '10' > 9               java.lang.IllegalArgumentException
---
> '10' > 9               java.lang.ClassCastException
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 flags_session.py

if ('false') ran its block. toBoolean() accepted 'y' and '1' but not 'yes', so a flag documented as “yes or no” needs its own check. Comparing two strings compares them character by character, so '10' > '9' is false, because '1' comes before '9'; convert first, and '10'.toInteger() > 9 is true. Comparing a String with a number failed on every runtime, with a different exception: IllegalArgumentException on 6.0.0, 5.1.3 and 4.0.33, and ClassCastException on 2.4.21, so a test that expects one class passes on one runtime and fails on the other.

if (env.SKIP_TESTS) for a flag

An environment value is text, so if (env.SKIP_TESTS) is true for 'false' and for '0'. Write if (env.SKIP_TESTS?.toBoolean()), which is false for a missing value, for 'false' and for '0'.

Checkpoint

Check: Flags and numbers that arrive as text

  1. One question An environment variable DRY_RUN holds the text 'no'. What do if (env.DRY_RUN) and env.DRY_RUN.toBoolean() give?

    Choose one answer.

    A hint

    One tests the text for being empty, the other reads its words.

    Show the answer: One question

    Answer: The if runs its block, and toBoolean() is false

    Text is true whenever it is not empty, so a flag must be converted. toBoolean() turns 'true', 'y' and '1' into true and everything else into false, as the session showed for 'yes', 'false', '0' and ''.

The safe navigation operator returns null instead of throwing

Configuration is full of values that may be missing: a map without the key proxy, a list that was never set. Reading config.proxy.host then throws a NullPointerException, because config.proxy is null. The safe navigation operator: The operator ?. in Groovy: a?.b reads b only when a is not null, and gives null otherwise instead of throwing a NullPointerException., ?., checks the value before the dot: if it is null, that step gives null instead of reading anything. Step through both reads:

Interactive · step by step Reading config.proxy.host with and without ?. Step through both reads. Watch where each one stops when proxy is missing.
Reading config.proxy.host with and without ?., step 5 of 5. Showing config, .proxy (null), ?.host (returns null), .host (NullPointerException). In focus: ?.host, .host.config[retry: …, name: ''].proxynull?.hostreturns null.hostNullPointerException

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

Every step as text
  1. The map. config is a map with the keys retry and name. It has no key proxy.
  2. config.proxy. Reading a key that a map does not have gives null, so config.proxy is null. Both reads get this far.
  3. With ?.. The safe navigation operator checks the value before it: proxy is null, so ?.host returns null without reading anything, and the session printed null.
  4. Without ?.. A plain .host asks null for a property, and Groovy throws NullPointerException with the message Cannot get property 'host' on null object.
  5. One ?. per step. Each ?. guards only the dot it replaces. config?.proxy.host would still throw, because the null appears after the safe dot, at .host.

safe.groovy GROOVY · safe.groovy
def config = [retry: [count: 0], name: '']
println "config.proxy?.host           ${config.proxy?.host}"
println "config.retry?.count          ${config.retry?.count}"
try {
    println config.proxy.host
} catch (NullPointerException e) {
    println "config.proxy.host            NullPointerException: ${e.message}"
}
List<Map> hosts = null
println "hosts*.name                  ${hosts*.name}"
println "[null, [name: 'db1']]*.name  ${[null, [name: 'db1']]*.name}"
Safe navigation and the spread operator, on four runtimes Python · safe_session.py
from session import jenkins_jar, run, use

# Safe navigation and the spread operator on Groovy 6.0.0, then on each older runtime: diff prints only the lines that differ from 6.0.0's.
run("""
groovy safe.groovy | tee on-6.0.0.txt
""")
for version in ("5.1.3", "4.0.33"):
    use(version)
    run(f"""
    groovy safe.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 safe.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 safe.groovy | tee on-6.0.0.txt
config.proxy?.host           null
config.retry?.count          0
config.proxy.host            NullPointerException: Cannot get property 'host' on null object
hosts*.name                  null
[null, [name: 'db1']]*.name  [null, db1]
# The groovy command is Groovy 5.1.3 from here on.
$ groovy safe.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 safe.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 safe.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 safe_session.py

config.proxy?.host printed null, while config.proxy.host threw Cannot get property 'host' on null object. config.retry?.count printed 0: ?. stops only at null, never at a zero or an empty value. The spread operator is null-safe in the same way, as the documentation says: hosts*.name on a null list gave null, and a list holding a null gave [null, db1]. All four runtimes printed the same lines, so ?. and *. are safe in a Jenkinsfile too.

Key idea

?. stops the expression at a null and gives null; it guards only the dot it replaces, and it never treats zero or empty values as missing.

The Elvis operators treat 0, empty text and false as missing

The Elvis operator: The operator ?: in Groovy: a ?: b gives a when a is true by the Groovy truth and b otherwise, so 0, empty text and false are replaced as well as null., value ?: fallback, gives the value when it is true and the fallback otherwise; the documentation describes it as a shortening of the ternary operator for “returning a ‘sensible default’ value”. Because it uses Groovy truth, a valid 0, an empty text or false count as missing too. Groovy 3 added the Elvis assignment x ?= fallback, which assigns only when x is false, and the safe index list?[0]:

elvis.groovy and elvisAssign.groovy

GROOVY (elvis.groovy) · elvis.groovy

def config = [retries: 0, name: '', timeout: null]
println "config.retries ?: 3          ${config.retries ?: 3}"
println "config.name ?: 'build'       ${config.name ?: 'build'}"
println "config.timeout ?: 30         ${config.timeout ?: 30}"
println "retries, with a null check   ${config.retries != null ? config.retries : 3}"

GROOVY (elvisAssign.groovy) · elvisAssign.groovy

def timeout = null
timeout ?= 30
def retries = 0
retries ?= 3
def hosts = null
println "timeout ?= 30 gives $timeout, retries ?= 3 gives $retries, hosts?[0] gives ${hosts?[0]}"
The Elvis operator on four runtimes Python · elvis_session.py
from session import jenkins_jar, run, use

# The Elvis operator on Groovy 6.0.0, then on each older runtime: diff prints only the lines that differ from 6.0.0's.
run("""
groovy elvis.groovy | tee on-6.0.0.txt
""")
for version in ("5.1.3", "4.0.33"):
    use(version)
    run(f"""
    groovy elvis.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 elvis.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 elvis.groovy | tee on-6.0.0.txt
config.retries ?: 3          3
config.name ?: 'build'       build
config.timeout ?: 30         30
retries, with a null check   0
# The groovy command is Groovy 5.1.3 from here on.
$ groovy elvis.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 elvis.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 elvis.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 elvis_session.py

?= and ?[] on four runtimes Python · assign_session.py
from session import jenkins_jar, run, use

# ?= and ?[] on Groovy 6.0.0, 5.1.3 and 4.0.33, then on Jenkins' 2.4.21, where only the error's first line is shown.
run("""
groovy elvisAssign.groovy
""")
for version in ("5.1.3", "4.0.33"):
    use(version)
    run("""
    groovy elvisAssign.groovy
    """)
jenkins_jar()
run("""
java -cp groovy-all-2.4.21.jar groovy.ui.GroovyMain elvisAssign.groovy 2>&1 | grep 'elvisAssign.groovy: 2:'
""")

Output

$ groovy elvisAssign.groovy
timeout ?= 30 gives 30, retries ?= 3 gives 3, hosts?[0] gives null
# The groovy command is Groovy 5.1.3 from here on.
$ groovy elvisAssign.groovy
timeout ?= 30 gives 30, retries ?= 3 gives 3, hosts?[0] gives null
# The groovy command is Groovy 4.0.33 from here on.
$ groovy elvisAssign.groovy
timeout ?= 30 gives 30, retries ?= 3 gives 3, hosts?[0] gives null
# 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 elvisAssign.groovy 2>&1 | grep 'elvisAssign.groovy: 2:'
elvisAssign.groovy: 2: unexpected token: ? @ line 2, column 9.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 assign_session.py

config.retries ?: 3 gave 3 although the setting was 0, and config.name ?: 'build' replaced an empty name, while config.timeout ?: 30 filled in a missing value as intended. The explicit check, config.retries != null ? config.retries : 3, kept the 0. retries ?= 3 replaced a 0 in the same way, and on Groovy 2.4.21 the file did not even compile: unexpected token: ? at line 2, the first ?=. Choose the form for your setting:

Interactive · answer and compare Which way should a setting get its default? Answer two questions about the setting. The table marks the forms that fit and says why the others do not.
Can the setting hold 0, an empty text or false as a real value?
Must the code also run in a Jenkinsfile or shared library?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Four ways to fill in a default, against the three things that decide between them
Form Keeps 0 and empty text Compiles on Groovy 2.4.21 Short to write
value ?: default Replaces: null, 0, empty text, false Poor retries 0 became 3 in the session Good gave the same results on 2.4.21 Good one operator
value ?= default Replaces: the same, in place Poor retries ?= 3 replaced 0 with 3 Poor unexpected token: ? on 2.4.21 Good one operator
value != null ? value : default Replaces: only null Good kept retries at 0 Good plain Groovy that every runtime reads Mixed names the value twice
if (value == null) value = default Replaces: only null Good replaces only a missing value Good plain Groovy that every runtime reads Mixed a statement of its own
How the answers decide
  • “Can the setting hold 0, an empty text or false as a real value?” Yes, 0 or empty can be meant: rules out value ?: default and value ?= default. Both treat 0, empty text and false as missing: retries 0 became 3 with ?: and with ?=.
  • “Must the code also run in a Jenkinsfile or shared library?” Yes, on Groovy 2.4.21: rules out value ?= default. ?= arrived in Groovy 3; Jenkins compiles Pipeline code with Groovy 2.4.21, which stops at it.
  • “Can the setting hold 0, an empty text or false as a real value?” No, only a missing value and “Must the code also run in a Jenkinsfile or shared library?” No, Groovy 3 or later only: adds a caveat to value != null ? value : default and if (value == null) value = default. They work, but ?: or ?= says the same in fewer characters when no valid value is falsy.

In short: ?: for text and objects where empty means missing; a null check where 0, '' or false are real values; and no ?= or ?[] in code that Jenkins runs.

Checkpoint

Check: Safe navigation and the spread operator · The Elvis operators and their zero trap

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    def build = [result: [tests: 0]]
    println build.result?.tests
    println build.agent?.name
    Show the answer: Question 1 of 2

    Answer: 0, then null

    The safe navigation operator checks only for null. A zero, an empty list or empty text pass through unchanged.

  2. Question 2 of 2 A setting parallelism may be 0, which means "run nothing in parallel". Which expression keeps 0 and defaults only a missing value to 4?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: parallelism != null ? parallelism : 4

    ?: and ?= use Groovy truth, so 0, empty text and false all count as missing. Compare with null when those values are meaningful.

Practise: true, false and null

Test what you can now predict about true, false and null. The practice opens where your earlier answers went wrong, and each explanation points back to a recorded run:

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 Is the text 'false' true or false in an if?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: True, because it is a non-empty String

    A CharSequence is false only when its length is 0, so 'false', '0' and ' ' are all true.

  2. Question 2 of 15 A helper tests a computed double with if (ratio). When ratio is Double.NaN, what happens on Groovy 6.0.0 and on Jenkins' 2.4.21?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: The block is skipped on 6.0.0 and runs on 2.4.21

    The runtimes disagree on NaN, so code that runs in more than one place should test what it means, such as Double.isNaN(ratio) or ratio > 0, instead of the truth of a double.

  3. Question 3 of 15 if (line =~ /ERROR/) is used to pick lines that contain ERROR. Which statement is right?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: It works, because a Matcher is true when it finds a match anywhere in the line

    Groovy gives a Matcher a truth of its own, so =~ reads naturally in a condition. Use ==~ only when the whole text must match.

  4. Question 4 of 15 Which method does a class define to decide its own truth?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: boolean asBoolean()

    Conditions, ! and ?: call asBoolean(), so defining it changes all of them at once, as !basket showed.

  5. Question 5 of 15 What does this print?

    Read the code, then choose one answer.

    class Queue {
        int waiting = 0
        boolean asBoolean() { waiting > 0 }
    }
    def q = new Queue(waiting: 3)
    println(q ? 'busy' : 'idle')
    q.waiting = 0
    println(q ?: 'no queue')
    Show the answer: Question 5 of 15

    Answer: busy, then no queue

    Defining asBoolean() changes every condition and every ?: that tests the object, as Basket's empty and full states did in the session.

  6. Question 6 of 15 A script moves from Groovy 4.0.33 to 6.0.0 and contains if (outputDir) { write(outputDir) }, where the folder is created by write(). What changes, and what do you write instead?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: On 6.0.0 the block is skipped while the folder does not exist; write if (outputDir != null)

    Groovy 5 changed what if (file) asks. Write the question you mean: != null for "is set", exists() for "is there". The system property groovy.truth.file.exists.enabled set to false restores the old rule while code is migrated.

  7. Question 7 of 15 Which texts does toBoolean() turn into true?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: 'true', 'y' and '1', ignoring case and surrounding spaces

    toBoolean() trims the text and compares it, ignoring case, with "true", "y" and "1"; anything else gives false.

  8. Question 8 of 15 A retry limit is read from the environment as text, and the code checks if (limit > '9'). For a limit of '10', what happens?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: The check is false, because the texts are compared character by character

    Two Strings compare lexicographically. Convert with toInteger(), after isInteger() if the text may not be a number, and '10'.toInteger() > 9 is true.

  9. Question 9 of 15 A test asserts that comparing a String with a number throws IllegalArgumentException. It passes locally on Groovy 5.1.3 but the same check fails when the code runs in Jenkins. Why?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: Jenkins' Groovy 2.4.21 throws ClassCastException for the same comparison

    The runtimes report the same mistake with different exception classes. Convert the text first instead of depending on the exception, or assert on the behaviour your code implements.

  10. Question 10 of 15 What does config.proxy.host do when config has no key proxy?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: It throws NullPointerException, Cannot get property 'host' on null object

    A missing key reads as null, and the next plain dot throws. ?. at that dot turns the whole expression into null.

  11. Question 11 of 15 List<Map> agents = null. What does agents*.name give?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: null

    *. gives null for a null receiver and null for each null element, so a following size() or each needs ?. as well.

  12. Question 12 of 15 settings?.proxy.host still throws NullPointerException for some jobs. Why?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: The ?. guards only the first dot; when proxy is null, the plain .host throws

    Put ?. at every dot whose left side can be null, or check once and handle the missing case explicitly.

  13. Question 13 of 15 What does name ?: 'build' give when name is ''?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: 'build'

    a ?: b gives a when a is true by the Groovy truth, otherwise b, so empty text, 0, false and null all give b.

  14. Question 14 of 15 A shared library line timeout ?= 30 works in tests on Groovy 5.1.3. What happens when Jenkins loads the library?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: It fails to compile with unexpected token ? on Groovy 2.4.21

    Write timeout = timeout ?: 30, or timeout != null ? timeout : 30 when 0 is a valid timeout, in code that Jenkins compiles.

  15. Question 15 of 15 A team writes def workers = settings.workers ?: 4 and someone sets workers to 0 to switch a stage off. Which fix keeps the code short, explicit and valid on every runtime?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: def workers = settings.workers != null ? settings.workers : 4

    When a falsy value is meaningful, the default must be chosen by a null check. The Tradeoffs table rules both Elvis forms out for such settings.

Interview questions

Warm-up (fresher): What does the Elvis operator do in Groovy, and when is it the wrong choice?

value ?: fallback gives value when it is true by the Groovy truth and fallback otherwise, so it is a short form of value ? value : fallback that names the value only once. It suits text and objects where empty or null both mean “not set”, such as name ?: 'build'. It is the wrong choice when 0, an empty text or false is a valid value: with retries = 0, retries ?: 3 gives 3, which silently turns “no retries” into three. There, write retries != null ? retries : 3. The Elvis assignment retries ?= 3 has the same trap, and it also needs Groovy 3 or later, so it fails to compile in a Jenkinsfile, which runs on Groovy 2.4.21.

Two longer interview 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 step runs if (env.SKIP_TESTS) { echo 'Skipping tests' } else { runTests() }. A job sets SKIP_TESTS to the text false, and the tests are still skipped. Explain why, fix the check, and say how you would make every flag of the shared library behave the same way.

    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 deploy helper reads int retries = config.retries ?: 3. A team sets retries to 0 to stop retrying a step that is not safe to repeat, and the step still runs three times. Explain the cause, write a fix that also compiles in a Jenkinsfile, and say how you would test it.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • null, zero and empty values are false and everything else is true, so 'false', '0' and [0] are true; Double.NaN is false on 4.0.33 and later but true on Jenkins’ 2.4.21.
  • A class defines its truth with asBoolean(), and from Groovy 5 a File is true only when the file exists, while 4.0.33 and 2.4.21 treat any non-null File as true.
  • Text must be converted before it is tested: toBoolean() accepts 'true', 'y' and '1', isInteger() guards toInteger(), and '10' > '9' is false as text.
  • ?. and *. turn a null into a null result instead of an exception, and they never treat 0 or empty as missing.
  • ?: and ?= replace 0, empty text and false as well as null, and ?= and ?[] do not compile on Groovy 2.4.21.

Terms in this lesson

asBoolean()
The method that Groovy calls to decide the truth of a value in if, while, ! and ?:. The Groovy Development Kit defines it for strings, numbers, collections, maps, matchers and, from Groovy 5, files; a class can define its own.
Elvis operator
The operator ?: in Groovy: a ?: b gives a when a is true by the Groovy truth and b otherwise, so 0, empty text and false are replaced as well as null.
Groovy truth
Groovy's rules for turning any value into true or false in a condition: null, zero and empty strings, collections, maps and arrays are false, and every other value is true unless its class defines otherwise.
safe navigation operator
The operator ?. in Groovy: a?.b reads b only when a is not null, and gives null otherwise instead of throwing a NullPointerException.

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.