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.

Language Atlas (mini-courses for more languages)  Module 8 – Groovy for Java developers, Jenkins and Gradle

Groovy for Java developers: idioms and traps

Port Java code to Groovy with def, GStrings, closures and literals, and predict ==, Groovy truth, ?:, BigDecimal division, power asserts and @Canonical.

  • Beginner
  • 20 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, groovy 6.0.0 and Java temurin-21.0.12+101.0.LTS
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Rewrite a Java class and its stream code in Groovy with def, GStrings, closures and collection literals
  • Predict what ==, is() and === return for strings, numbers and lists
  • Predict Groovy truth, and give a missing value a default without replacing a real 0
  • Predict the results of /, intdiv, round and integer overflow, and pick the operation the Java code meant
  • Read a failing power assertion and replace boilerplate with @Canonical

Before you start

What you’ll learn · Before you start

What you will learn

  • Rewrite a Java class and its stream code in Groovy with def, GStrings, closures and collection literals
  • Predict what ==, is() and === return for strings, numbers and lists
  • Predict Groovy truth, and give a missing value a default without replacing a real 0
  • Predict the results of /, intdiv, round and integer overflow, and pick the operation the Java code meant
  • Read a failing power assertion and replace boilerplate with @Canonical

Before you start

On this page

7 / 2 is 3 in Java and 3.5 in Groovy, and 0.1 + 0.2 == 0.3 is false in Java and true in Groovy. Most Java compiles as Groovy, so a port rarely fails to build; it fails on lines like these, which build and then mean something else.

This lesson maps Java habits onto Groovy one at a time, each shown by a recorded run on Groovy 6.0.0 and JDK 21: a Java class with streams next to its Groovy port, the GDK methods that replace streams, what == compares, Groovy truth with ?. and ?:, the rules of division and rounding, and the two tools that make Groovy pleasant to debug, the power assertion and @Canonical.

A Java class and its streams become a few Groovy lines

Builds.java summarises four CI builds the Java 21 way: a record, three stream pipelines and a loop that groups the builds by result. builds.groovy does the same in Groovy. Read the two side by side:

The same summary in Java and in Groovy

port/Builds.java

import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

// Four CI builds, summarised the Java 21 way: a record, streams and a loop to group them.
public class Builds {
    record Build(String name, int minutes, boolean green) {}

    public static void main(String[] args) {
        List<Build> builds = List.of(
                new Build("api", 12, true),
                new Build("web", 18, false),
                new Build("docs", 4, true),
                new Build("etl", 11, false));

        List<String> green = builds.stream().filter(Build::green).map(Build::name).toList();
        int total = builds.stream().mapToInt(Build::minutes).sum();
        Map<Boolean, List<String>> byResult = new LinkedHashMap<>();
        for (Build b : builds) {
            byResult.computeIfAbsent(b.green(), k -> new ArrayList<>()).add(b.name());
        }
        Build slowest = builds.stream().max(Comparator.comparingInt(Build::minutes)).orElseThrow();

        System.out.println(green);
        System.out.println(total);
        System.out.println(byResult);
        System.out.println(slowest);
        System.out.println("Total: " + total + " minutes over " + builds.size() + " builds");
    }
}

port/builds.groovy

import groovy.transform.Canonical

// The same four builds in Groovy: @Canonical writes the constructor, equals, hashCode and toString.
@Canonical
class Build {
    String name
    int minutes
    boolean green
}

def builds = [
    new Build('api', 12, true),
    new Build('web', 18, false),
    new Build('docs', 4, true),
    new Build('etl', 11, false),
]

def green = builds.findAll { it.green }*.name
def total = builds.sum { it.minutes }
def byResult = builds.groupBy { it.green }.collectEntries { result, list -> [result, list*.name] }
def slowest = builds.max { it.minutes }

println green
println total
println byResult
println slowest
println "Total: $total minutes over ${builds.size()} builds"

The Groovy version has no imports for collections, because Groovy imports java.util and several other packages by default, as its page on the differences with Java says. It declares variables with def, which the semantics guide calls “strictly equivalent to using Object”: that is optional typing: Writing a type only where it helps, and def where it does not. In Groovy def is the same as Object, and a call on such a variable finds its method while the program runs., with a type where it helps, such as int minutes in the class, and def where it does not. A list is written in square brackets and is a java.util.ArrayList. Each pipeline is one method call with a closure, and the last line is a GString: A double-quoted Groovy string with placeholders, $name or ${expression}, whose values are filled in when the string is used. A double-quoted string without placeholders, and any single-quoted string, is a plain java.lang.String., a double-quoted string with $total and ${builds.size()} in it. Both programs ran on the same JDK 21:

Builds.java and builds.groovy on the same JDK 21 Python · compare_session.py
from session import run

# The Java program and its Groovy port, run one after the other on the same JDK 21, then their sizes in lines.
run("""
cd port
java Builds.java
groovy builds.groovy
wc -l Builds.java builds.groovy
""")

Output

$ cd port
$ java Builds.java
[api, docs]
45
{true=[api, docs], false=[web, etl]}
Build[name=web, minutes=18, green=false]
Total: 45 minutes over 4 builds
$ groovy builds.groovy
[api, docs]
45
[true:[api, docs], false:[web, etl]]
Build(web, 18, false)
Total: 45 minutes over 4 builds
$ wc -l Builds.java builds.groovy
      32 Builds.java
      27 builds.groovy
      59 total

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

Three of the five lines are the same. The other two show that Groovy prints values its own way:

Value Java printed Groovy printed
a map {true=[api, docs], false=[web, etl]} [true:[api, docs], false:[web, etl]]
the slowest build Build[name=web, minutes=18, green=false] Build(web, 18, false)

The map is written the way a Groovy map literal is written, and Build(web, 18, false) is the toString that @Canonical generated, the subject of the last section. The port is 27 lines against 32, and most of what is left is the data. Shorter is not the point, though: a test that compares printed output with what the Java version printed would fail on those two lines, and the practice lab at the end ports that printing to Python.

Checkpoint

Check: Porting a Java class and its streams

  1. Question 1 of 2 What type does a variable declared with def have in Groovy?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Object: def is the same as declaring the variable Object

    def is optional typing: no type is written, so the variable is an Object, and Groovy finds each method on the value it holds when the call runs.

  2. Question 2 of 2 A test compares the output of builds.groovy with what Builds.java printed. Which line fails the comparison?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The grouped map: Groovy printed [true:[api, docs], false:[web, etl]], Java {true=[api, docs], false=[web, etl]}

    Three lines matched and two did not: the map and the build, which @Canonical printed as Build(web, 18, false) and the Java record as Build[name=web, minutes=18, green=false].

Closures and the GDK methods do the work of streams

Groovy adds methods to the JDK’s own classes, the Groovy Development Kit or GDK, and most of them take a closure: findAll, collect, sum, groupBy, max, every, any. A closure with no parameter list gets one implicit parameter, it, which the closures guide describes as “strictly equivalent” to declaring it ->. Step through the calls of builds.groovy on its four builds:

Interactive · step by step Four builds through the calls of builds.groovy Step through each call on the list of four builds and the value it returns, as the script printed it.
Four builds through the calls of builds.groovy, step 6 of 6. Showing The four builds, findAll { it.green }, *.name, sum { it.minutes }, groupBy { it.green }, max { it.minutes }. In focus: max { it.minutes }.The four buildsapi, web, docs, etlfindAll { it.green }keeps api and docs*.name[api, docs]sum { it.minutes }adds up to 45groupBy { it.green }a map: true and falsemax { it.minutes }Build(web, 18, false)

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

Every step as text
  1. Four builds. A list literal of four Build objects, each with a name, its minutes and whether it was green. Each of the four lines that follow it in the script reads this one list and changes nothing in it.
  2. Keep the green ones. findAll runs the closure for each build and keeps those for which it returns true. it is the build the closure is given: two of the four are green.
  3. One property of each. The spread operator *.name reads name from every build left and collects the results: [api, docs], the first line the script printed.
  4. Add up the minutes. sum with a closure adds what the closure returns for each build: 12 + 18 + 4 + 11 = 45, an Integer, the second line.
  5. Group by a property. groupBy reads the same four builds and makes a map from each value the closure returns to the builds that returned it; collectEntries then keeps only their names: [true:[api, docs], false:[web, etl]].
  6. The slowest build. max with a closure compares the four builds by what the closure returns and gives back the whole build: Build(web, 18, false), in the format @Canonical wrote for toString.

The spread operator: Groovy's *. operator: list*.name reads name from every item of the list and collects the results in a new list. An item that is null gives null instead of an exception. *.name reads one property from every item, which is what map(Build::name) did in Java. Each call returns a new list or map and leaves builds as it was, as streams do. Two things differ from streams, and they matter at the edges, with an empty list or a null inside one:

edges.groovy GROOVY · gdk/edges.groovy
// The GDK methods on lists that are empty, or hold a null: what comes back is not always what Java streams return.
def minutes = [12, 18, 4, 11]
def none = []
println "sum of the minutes: ${minutes.sum()}, of no minutes: ${none.sum()}, with a start value: ${none.sum(0)}"
println "max of no minutes: ${none.max()}, first of no minutes: ${none.find { true }}"
println "doubled: ${minutes.collect { it * 2 }}, with a named parameter: ${minutes.collect { m -> m * 2 }}"
def names = ['api', null, 'docs']
println "spread over a null: ${names*.toUpperCase()}"
println "every, any: ${minutes.every { it > 3 }}, ${minutes.any { it > 15 }}"
GDK calls on an empty list and on a list with a null Python · gdk_session.py
from session import run

run("""
cd gdk
groovy edges.groovy
""")

Output

$ cd gdk
$ groovy edges.groovy
sum of the minutes: 45, of no minutes: null, with a start value: 0
max of no minutes: null, first of no minutes: null
doubled: [24, 36, 8, 22], with a named parameter: [24, 36, 8, 22]
spread over a null: [API, null, DOCS]
every, any: true, true

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

sum() of an empty list returned null, where Java’s IntStream.sum() returns 0, and max() of an empty list returned null as well, where Java’s max() returns an empty Optional. sum(0) starts from 0 and returns 0 for no items. The spread operator is null-safe, as the operators guide says: the null among the names stayed null instead of throwing.

Adding up a list that can be empty

def total = builds.sum { it.minutes } gives null when no build matched, and the next line, total / builds.size(), throws a NullPointerException. Give sum a start value, builds.sum(0) { it.minutes }, wherever the list can be empty, which is almost everywhere a filter came first.

Checkpoint

Check: Closures and GDK methods

  1. One question def late = builds.findAll { it.minutes > 60 } finds no build. What does late.sum { it.minutes } return?

    Choose one answer.

    Show the answer: One question

    Answer: null

    An empty list has nothing to add, and Groovy's sum returns null for it. Write late.sum(0) { it.minutes } when the list can be empty.

Equality compares values in Groovy, and is() compares identity

Java’s == between two references is true only for one and the same object. Groovy’s page on the differences with Java puts it in one line, “== means equality in all places”: for a type that implements Comparable it calls compareTo, and for any other type equals. Identity has its own spellings, the method is() and, since Groovy 3, the operator ===. The probe compares strings built two ways, two BigDecimal values, an Integer with a Long, and two lists:

equality.groovy GROOVY · equality/equality.groovy
// What == means in Groovy: equals(), or compareTo() for Comparable values. is() and === test identity.
def typed = 'invoice'
def built = new StringBuilder('in').append('voice').toString()
println "typed == built       ${typed == built}"
println "typed.is(built)      ${typed.is(built)}"
println "typed === built      ${typed === built}"

def a = new BigDecimal('1.0')
def b = new BigDecimal('1.00')
println "1.0 == 1.00          ${a == b}"
println "1.0.equals(1.00)     ${a.equals(b)}"
println "1.0 <=> 1.00         ${a <=> b}"

Integer i = 7
Long l = 7L
println "Integer 7 == Long 7  ${i == l}"
println "i.equals(l)          ${i.equals(l)}"
println "[1, 2] == [1, 2]     ${[1, 2] == [1, 2]}"
==, equals, is and === on strings, numbers and lists Python · equality_session.py
from session import run

run("""
cd equality
groovy equality.groovy
""")

Output

$ cd equality
$ groovy equality.groovy
typed == built       true
typed.is(built)      false
typed === built      false
1.0 == 1.00          true
1.0.equals(1.00)     false
1.0 <=> 1.00         0
Integer 7 == Long 7  true
i.equals(l)          false
[1, 2] == [1, 2]     true

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

Comparison Groovy == Java habit
a typed string and an equal built one true == is false in Java, so Java code calls equals
the same two strings with is() or === false Java’s ==
1.0 and 1.00 as BigDecimal true, by compareTo equals is false: the scales differ
Integer 7 and Long 7 true equals is false: the classes differ
[1, 2] and [1, 2] true List.equals

The BigDecimal row is the one that surprises Java developers in both directions. equals compares the value and the scale, so 1.0 and 1.00 are not equals; Groovy’s == uses compareTo, which compares the value only, and <=> printed 0. So == in Groovy can be true where equals in the same program is false, and a HashSet or a map key, which use equals, can still keep both values.

Key idea

In Groovy, == compares values, through compareTo for comparable types and equals for the rest; ask for identity with is() or ===.

Checkpoint

Check: What == and is() compare

  1. Question 1 of 2 typed is 'invoice' and built is the same text made with a StringBuilder. What does typed === built print?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: false: === tests identity, and these are two objects

    == compares values, so typed == built is true. is() and === ask whether two variables hold the same object, which they do not.

  2. Question 2 of 2 def a = new BigDecimal('1.0') and def b = new BigDecimal('1.00'). What do a == b and a.equals(b) give?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: a == b is true, a.equals(b) is false

    The probe printed 1.0 == 1.00 true, equals false and <=> 0. A HashSet, which uses equals, would keep both.

Groovy truth decides every condition, and the Elvis operator uses it

Java’s if takes only a boolean. Groovy’s takes any value and decides by Groovy truth: The rules by which any Groovy value counts as true or false in a condition: null, zero, empty strings, empty collections and maps and a matcher without a match are false; non-empty text, even 'false', is true., the rules of its semantics guide: null is false, so are empty strings, collections and maps, the number zero and a regular-expression matcher without a match; nearly everything else is true. The probe decides 16 values the way if would, with value ? 'true' : 'false':

truth.groovy GROOVY · truth/truth.groovy
// The Groovy truth of 16 values: what if (value) decides for each.
def values = [
    'null'          : null,
    "''"            : '',
    "' '"           : ' ',
    "'false'"       : 'false',
    "'0'"           : '0',
    '0'             : 0,
    '0.0'           : 0.0,
    '-1'            : -1,
    '[]'            : [],
    '[0]'           : [0],
    '[:]'           : [:],
    '[a: null]'     : [a: null],
    'false'         : false,
    "'x' =~ /y/"    : 'x' =~ /y/,
    "'x' =~ /x/"    : 'x' =~ /x/,
    'new Object()'  : new Object(),
]
values.each { shown, value ->
    println "${shown.padRight(14)} ${value ? 'true' : 'false'}"
}
The truth of 16 values, then a default written two ways Python · truth_session.py
from session import run

run("""
cd truth
groovy truth.groovy
groovy elvis.groovy
""")

Output

$ cd truth
$ groovy truth.groovy
null           false
''             false
' '            true
'false'        true
'0'            true
0              false
0.0            false
-1             true
[]             false
[0]            true
[:]            false
[a: null]      true
false          false
'x' =~ /y/     false
'x' =~ /x/     true
new Object()   true
$ groovy elvis.groovy
api: ?: gives 12, a null test gives 12
docs: ?: gives 30, a null test gives 0
web: ?: gives 30, a null test gives 30
no build: ?: gives 30, a null test gives 30

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

The table has three rows that catch people out: the text 'false' and the text '0' are true, because they are non-empty strings, and the map [a: null] is true, because it has one entry. Two operators build on these rules. safe navigation operator: Groovy's ?. operator: a?.b reads b from a, or gives null when a is null instead of throwing a NullPointerException., build?.minutes, gives null instead of throwing when build is null. The Elvis operator: Groovy's a ?: b, which gives a when a is true under Groovy truth and b otherwise. It replaces null, but also 0, empty text and false, so it is no test for null alone., a ?: b, gives a when a is true and b otherwise. Together they make the usual line for a default, build?.minutes ?: 30, and the second program ran that line on four builds. Step through what it decided for each:

Interactive · step by step What build?.minutes ?: 30 does with four builds Step through the four builds of elvis.groovy and watch which side of ?: each one takes.
What build?.minutes ?: 30 does with four builds, step 5 of 5. Showing build?.minutes, Is the value true? (m != null instead), Keep the value (docs keeps its 0), Use the default 30 (only when missing). In focus: Is the value true?.truefalsebuild?.minutesnull when build is nullIs the value true?m != null insteadKeep the valuedocs keeps its 0Use the default 30only when missing

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

Every step as text
  1. api, 12 minutes. build?.minutes reads 12. A non-zero number is true under Groovy truth, so ?: keeps it: api gives 12 either way.
  2. docs, 0 minutes. 0 minutes is a real value, but zero is false under Groovy truth, so ?: takes the right side and docs gets 30. This is the bug.
  3. web, no minutes. The map has no minutes key, so build?.minutes is null. null is false too, and 30 is the default the code wanted here.
  4. No build at all. build itself is null, so ?. returns null instead of throwing a NullPointerException, and ?: gives 30 again.
  5. The null test. m != null ? m : 30 asks only whether the value is missing. It keeps the 0 of docs and gives 30 to web and to the missing build, as the second column printed.

The docs build took 0 minutes, and ?: replaced its 0 with 30, because 0 is false. For web, which had no minutes, and for the missing build, 30 was the right answer. A test for null alone, m != null ? m : 30, gave all four right.

Using ?: for a default when 0 or empty text is a real value

build?.minutes ?: 30 turns a 0-minute build into a 30-minute one, and name ?: 'unknown' turns an empty name into unknown. Where 0, '' or false can be real values, test for null instead: def m = build?.minutes; m != null ? m : 30.

Checkpoint

Check: Groovy truth, ?. and ?:

  1. Question 1 of 2 A flag read from a config file holds the text 'false'. What does if (flag) do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It runs the block: a non-empty string is true

    Convert text to a boolean on purpose, for example flag.toBoolean() or flag == 'true', before testing it.

  2. Question 2 of 2 def timeout = config?.timeout ?: 60, and the config sets timeout to 0, meaning no limit. What is timeout?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 60: 0 is false under Groovy truth, so ?: takes the default

    Write def t = config?.timeout; t != null ? t : 60 when 0 is a real value.

Division makes a BigDecimal, and intdiv divides as Java does

Groovy’s syntax guide makes a decimal literal such as 0.1 a BigDecimal, and its division operator gives a double if either side is a float or a double and a BigDecimal otherwise, also for two whole numbers. A division that does not end, such as 1 by 3, keeps 10 decimal places. Java divides two int values by dropping the fraction. The probe goes through the rows of the concept map: A table of the questions on which languages differ in meaning, such as how whole numbers divide or how a half rounds, answered for each new language with a run of your own. More in “A one-week plan for learning any language” that a port gets wrong:

numbers.groovy GROOVY · numbers/numbers.groovy
// Division, rounding and overflow: the rows of the concept map that a Java port gets wrong.
println "7 / 2             = ${7 / 2} (${(7 / 2).getClass().simpleName})"
println "7.intdiv(2)       = ${7.intdiv(2)}"
println "(-7).intdiv(2)    = ${(-7).intdiv(2)}"
println "-7 % 2            = ${-7 % 2}"
println "1 / 3             = ${1 / 3}"
println "2 / 3 * 3         = ${2 / 3 * 3}"
println "7.0d / 2          = ${7.0d / 2} (${(7.0d / 2).getClass().simpleName})"
println "0.1 + 0.2 == 0.3  = ${0.1 + 0.2 == 0.3}"
println "0.1d + 0.2d       = ${0.1d + 0.2d}"
println "2.5.round()       = ${2.5.round()}, (-2.5).round() = ${(-2.5).round()}"
println "Math.round(2.5)   = ${Math.round(2.5)}, Math.round(-2.5) = ${Math.round(-2.5)}"
int most = Integer.MAX_VALUE
println "MAX_VALUE + 1     = ${most + 1}"
def big = 2147483648
println "2147483648        is a ${big.getClass().simpleName}"
Division, rounding and overflow on Groovy 6.0.0 Python · numbers_session.py
from session import run

run("""
cd numbers
groovy numbers.groovy
""")

Output

$ cd numbers
$ groovy numbers.groovy
7 / 2             = 3.5 (BigDecimal)
7.intdiv(2)       = 3
(-7).intdiv(2)    = -3
-7 % 2            = -1
1 / 3             = 0.3333333333
2 / 3 * 3         = 2.0000000001
7.0d / 2          = 3.5 (Double)
0.1 + 0.2 == 0.3  = true
0.1d + 0.2d       = 0.30000000000000004
2.5.round()       = 3, (-2.5).round() = -3
Math.round(2.5)   = 3, Math.round(-2.5) = -2
MAX_VALUE + 1     = -2147483648
2147483648        is a Long

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

Row of the concept map Groovy 6.0.0 Java
7 / 2 3.5, a BigDecimal 3
integer division 7.intdiv(2) is 3, (-7).intdiv(2) is −3 7 / 2 is 3, -7 / 2 is −3
remainder -7 % 2 is −1 −1
2 / 3 * 3 2.0000000001 0 with ints
0.1 + 0.2 == 0.3 true: decimal literals are BigDecimal false with double
a half, rounded 2.5.round() is 3, (-2.5).round() is −3 Math.round(-2.5) is −2
Integer.MAX_VALUE + 1 −2147483648, it wraps the same
the literal 2147483648 a Long a compile error without L

2 / 3 * 3 printed 2.0000000001. The division kept 10 decimals, 0.6666666667, and the multiplication by 3 made the rounding visible: BigDecimal is exact for adding, subtracting and multiplying, not for a division that never ends. intdiv is Java’s integer division, truncating towards zero, and % keeps Java’s sign rule. The rounding rows differ too: round() on a BigDecimal sent −2.5 to −3, away from zero, while Java’s Math.round, whose documentation says ties round “to positive infinity”, gave −2. Overflow stays as in Java: an int that passes its largest value wraps silently, while a literal too big for an int simply becomes a Long.

Rounding Calculator Round 2.5 and -2.5 to whole numbers: half away from zero gives 3 and -3, as Groovy's round() did, and half up, toward positive infinity, gives 3 and -2, as Math.round did.

Porting Java's integer division with /

def perBuild = totalMinutes / builds gives 11.25 in Groovy where the Java code computed 11. If the Java code meant a whole number, write totalMinutes.intdiv(builds); if it meant a real average, keep / and round on purpose with setScale(2, RoundingMode.HALF_UP), after import java.math.RoundingMode, which Groovy does not import by default.

Checkpoint

Check: Division, rounding and overflow

  1. Question 1 of 2 The Java code computes int perRun = total / runs with total = 45 and runs = 4. Which Groovy expression gives the same 11?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: total.intdiv(runs)

    Java's integer division drops the fraction. Groovy's / keeps it, as a BigDecimal, and intdiv drops it as Java does.

  2. Question 2 of 2 How many decimal places did Groovy keep for 1 / 3, which printed 0.3333333333?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 10 decimal places

    A BigDecimal division that never ends keeps a scale of at least 10. That is why 2 / 3 * 3 printed 2.0000000001: the 10th decimal of 0.6666666667 was rounded up before the multiplication.

A power assertion shows every value, and @Canonical writes the class

Groovy’s assert is always on, and when it fails it does not just say “false”. The testing guide calls it a power assertion: Groovy's assert statement, which is always on: when its condition is false it prints the expression with the value of every part that was evaluated written under it.: it prints the expression with the value of each part under it. canonical.groovy catches a failing one and prints its message, after using a class whose boilerplate an AST transformation: Code that changes a program while the compiler builds it, switched on by an annotation such as @Canonical, which adds constructors, equals, hashCode and toString to a class. wrote. The session then compiles the file and lists that class with javap:

canonical.groovy GROOVY · boilerplate/canonical.groovy
import groovy.transform.Canonical

// @Canonical generates the constructor, equals, hashCode and toString; a failing assert prints every value.
@Canonical
class Build {
    String name
    int minutes
    boolean green
}

def first = new Build('api', 12, true)
def again = new Build('api', 12, true)
println first
println "equal: ${first == again}, same object: ${first.is(again)}"
println "distinct builds in a set: ${[first, again, new Build('web', 18, false)] as Set}"

def builds = [first, new Build('web', 18, false), new Build('docs', 4, true)]
try {
    assert builds.every { it.green } && builds.sum { it.minutes } < 30
} catch (AssertionError e) {
    println e.message
}
@Canonical in use, a power assertion, and the class javap lists Python · boilerplate_session.py
from session import run

# The script, then the class @Canonical made: compiled with groovyc and listed with javap.
run("""
cd boilerplate
groovy canonical.groovy
groovyc canonical.groovy
javap -public Build.class
""")

Output

$ cd boilerplate
$ groovy canonical.groovy
Build(api, 12, true)
equal: true, same object: false
distinct builds in a set: [Build(api, 12, true), Build(web, 18, false)]
assert builds.every { it.green } && builds.sum { it.minutes } < 30
       |      |                  |
       |      false              false
       [Build(api, 12, true), Build(web, 18, false), Build(docs, 4, true)]
$ groovyc canonical.groovy
$ javap -public Build.class
Compiled from "canonical.groovy"
public class Build implements groovy.lang.GroovyObject {
  public static transient boolean __$stMC;
  public Build(java.lang.String, int, boolean);
  public Build(java.lang.String, int);
  public Build(java.lang.String);
  public Build();
  public java.lang.String toString();
  public int hashCode();
  public boolean canEqual(java.lang.Object);
  public boolean equals(java.lang.Object);
  public groovy.lang.MetaClass getMetaClass();
  public void setMetaClass(groovy.lang.MetaClass);
  public java.lang.String getName();
  public void setName(java.lang.String);
  public int getMinutes();
  public void setMinutes(int);
  public boolean getGreen();
  public boolean isGreen();
  public void setGreen(boolean);
  public boolean super$1$equals(java.lang.Object);
  public int super$1$hashCode();
  public java.lang.String super$1$toString();
}

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

The assertion builds.every { it.green } && builds.sum { it.minutes } < 30 failed, and the message shows why: under builds is the list, under every its result, false, and under && the result of the whole expression. Nothing is shown under the second half, because && never ran it: the power assertion shows what was evaluated, and leaves out what was not.

@Canonical is one annotation for three, the metaprogramming guide says: @ToString, @EqualsAndHashCode and @TupleConstructor. javap listed what it wrote into Build: four constructors, from all three properties down to none, and toString, equals, hashCode and canEqual; the getter and the setter of each property are Groovy’s own, written for every property of a class. That is why two builds with the same values were equal, why the set kept two of the three, and why the build printed as Build(api, 12, true). In Java the same class is a record, or several dozen lines written by hand.

Read a failing assertion from the top

Read the values under a failing power assertion from the outermost part inwards: the first false from the left under an operator names the part that failed, and the values under its operands say why.

Checkpoint

Check: Power assertions and @Canonical

  1. Question 1 of 2 The power assertion of canonical.groovy showed no values under builds.sum { it.minutes } < 30. Why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: && stopped after its left side was false, so the right side never ran

    Read the values under a failing assertion as a record of the evaluation: what is missing was skipped.

  2. Question 2 of 2 Two Build objects made with new Build('api', 12, true) are compared with ==. With @Canonical on the class, what is the result?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: true: @Canonical generated equals and hashCode from the three properties

    @Canonical combines @ToString, @EqualsAndHashCode and @TupleConstructor, so the class compares, hashes and prints by its properties.

Practise: port two Groovy habits to Python exactly

When a Groovy tool is ported, or checked against a second implementation, two habits break the comparison first: the way Groovy prints lists and maps, and a default written with ?:. The lab writes both in Python, so that the sample tests run here in the browser; each test is a line that this lesson’s sessions printed:

Code lab · 2 steps

Port two Groovy habits to Python exactly

Print values the way Groovy's println shows them, then give missing minutes a default without turning a real 0 into one.

  1. Print values the way Groovy's println shows them To do
  2. Give missing minutes a default, and keep a real 0 To do

Exercise · Easy · Python

Print values the way Groovy's println shows them

A Java or Python port of a Groovy script often prints the same values in another shape, and a test that compares the output then fails. Write groovy_format(value) in groovy_format.py: it returns the text Groovy's println shows for the Groovy value that value stands for.

  • None is null, and True and False are true and false.
  • Numbers are written as Python writes them: 45, 3.5.
  • Text is written as it is, without quotes, also inside a list or a map: ['api', 'docs'] is [api, docs].
  • A list is its items, formatted the same way, between [ and ], separated by , .
  • A dict is a Groovy map: key:value pairs between [ and ], separated by , , with keys and values formatted the same way, and [:] when it is empty.

The tests use lines that builds.groovy and edges.groovy printed in this lesson.

Starter code · groovy_format.py

def groovy_format(value):
    """The text Groovy's println shows for the value: null, true, [a, b], [k:v], [:]."""
    # Replace this with your code: str() writes None, True and quotes the way Python does.
    return str(value)
The sample tests · test_groovy_format.py
from groovy_format import groovy_format


def test_the_green_names():
    """findAll { it.green }*.name printed [api, docs]"""
    assert groovy_format(["api", "docs"]) == "[api, docs]"


def test_the_grouped_map():
    """groupBy and collectEntries printed [true:[api, docs], false:[web, etl]]"""
    assert groovy_format({True: ["api", "docs"], False: ["web", "etl"]}) == "[true:[api, docs], false:[web, etl]]"


def test_a_null_inside_a_list():
    """the spread over a list with a null printed [API, null, DOCS]"""
    assert groovy_format(["API", None, "DOCS"]) == "[API, null, DOCS]"


def test_empty_list_and_empty_map():
    """an empty map is written [:], the literal that makes one"""
    assert groovy_format([]) == "[]"
    assert groovy_format({}) == "[:]"


def test_single_values():
    """numbers as they are, null for None"""
    assert groovy_format(45) == "45"
    assert groovy_format(None) == "null"
    assert groovy_format(False) == "false"


def test_nesting():
    """lists and maps inside each other"""
    assert groovy_format([[1, 2], {"a": None}]) == "[[1, 2], [a:null]]"
A hint

Test for bool before int: in Python True is also an int. Then call groovy_format again for every item of a list and for every key and value of a dict, and join the pieces.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Exercise · Easy · Python

Give missing minutes a default, and keep a real 0

In elvis.groovy, build?.minutes ?: 30 gave the docs build, which took 0 minutes, a default of 30, because 0 is false under Groovy truth. Write the corrected rule in Python, minutes_or_default(build, default=30) in minutes.py:

  • build is a dict such as {"name": "docs", "minutes": 0}, or None when there is no build;
  • return build["minutes"] when the build exists and has minutes that are not None, also when they are 0;
  • return default otherwise: no build, no minutes key, or minutes set to None.

The starter is the direct port of the Groovy line, and it repeats the mistake.

Starter code · minutes.py

def minutes_or_default(build, default=30):
    """The build's minutes, or the default when the build or its minutes are missing; a real 0 stays 0."""
    # Replace this with your code: this is the direct port of build?.minutes ?: 30, and it turns 0 into 30.
    return (build or {}).get("minutes") or default
The sample tests · test_minutes.py
from minutes import minutes_or_default


def test_api_keeps_its_minutes():
    """api, 12 minutes: both versions in elvis.groovy gave 12"""
    assert minutes_or_default({"name": "api", "minutes": 12}) == 12


def test_docs_keeps_its_zero():
    """docs, 0 minutes: ?: gave 30, the null test gave 0"""
    assert minutes_or_default({"name": "docs", "minutes": 0}) == 0


def test_web_without_minutes_gets_the_default():
    """web has no minutes key"""
    assert minutes_or_default({"name": "web"}) == 30


def test_no_build_gets_the_default():
    """the null at the end of the list"""
    assert minutes_or_default(None) == 30


def test_minutes_set_to_none_and_another_default():
    """a key that holds None counts as missing, and the default can change"""
    assert minutes_or_default({"name": "etl", "minutes": None}, default=15) == 15
A hint

Ask about None only: if build is None for the build, and build.get("minutes") compared with None for the minutes. Python's or decides by truth, as Groovy's ?: does, so it turns 0 into the default.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then practise the six ideas, from the port to @Canonical: questions arrive one by one, your weakest idea leads, and each answer you choose comes with its reason:

Practice that adapts to you

Practise this lesson

12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 Which of these is a GString?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: "Total: $total minutes"

    Groovy's syntax guide makes double-quoted strings GStrings only when they contain a placeholder, ${…} or $name.

  2. Question 2 of 12 A Java loop groups build names by result into a LinkedHashMap with computeIfAbsent. Which Groovy line gives the same map?

    Read the code, then choose one answer.

    // builds is a list of Build objects with name and green
    // wanted: [true:[api, docs], false:[web, etl]]
    Show the answer: Question 2 of 12

    Answer: builds.groupBy { it.green }.collectEntries { k, v -> [k, v*.name] }

    groupBy keeps every item under the value the closure returned, in order of first appearance; collectEntries rebuilds the map with new values.

  3. Question 3 of 12 In builds.findAll { it.green }, what is it?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: The closure's implicit parameter: the build findAll is testing

    { it.green } is short for { b -> b.green }; name the parameter when the closure is longer than one line.

  4. Question 4 of 12 names is ['api', null, 'docs']. What does names*.toUpperCase() return?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: [API, null, DOCS]

    list*.method() calls the method on every non-null item and keeps a null where an item was null.

  5. Question 5 of 12 In Groovy, what does Integer 7 == Long 7 give?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: true

    Numbers of different classes are compared by value in Groovy; equals still compares the class.

  6. Question 6 of 12 A Groovy method compares two amounts with if (a == b), and a cache keys its entries by the same BigDecimal values. 1.0 and 1.00 arrive. What happens?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: The check finds them equal, but the cache stores two entries, because it uses equals and hashCode

    Strip the scale before the values become keys, for example with stripTrailingZeros(), or key by a String formatted with a fixed scale.

  7. Question 7 of 12 Which of these values are false under Groovy truth?

    Choose every answer that is right.

    Show the answer: Question 7 of 12

    Answer:

    • ''
    • 0
    • [:]

    null, zero, empty strings, empty collections and maps, and a matcher with no match are false; non-empty text is true, even 'false' and '0'.

  8. Question 8 of 12 Retries may be 0 (no retry) or missing. Which line gives 3 only when retries is missing?

    Read the code, then choose one answer.

    def job = [name: 'api', retries: 0]
    Show the answer: Question 8 of 12

    Answer: def r = job?.retries; def retries = r != null ? r : 3

    When 0, empty text or false are real values, write the null test out; ?: is for values whose false cases are all missing.

  9. Question 9 of 12 What does 10 / 4 give in Groovy?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: 2.5, a BigDecimal

    / on whole numbers and BigDecimal values gives a BigDecimal, and on a float or double a double.

  10. Question 10 of 12 A Java service rounded -2.5 with Math.round and got -2. The Groovy port writes (-2.5).round(). What does it give?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: -3: the BigDecimal is rounded away from zero

    -2.5 is a BigDecimal literal in Groovy, and its round() rounded the half away from zero. When the port must match Java, call Math.round on a double, or choose the rounding mode with setScale.

  11. Question 11 of 12 What does a power assertion print when it fails?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: The expression, with the value of each evaluated part under it

    The testing guide calls Groovy's assert a power assertion: its message is the expression with every evaluated value under it.

  12. Question 12 of 12 javap listed four constructors in the Build class that @Canonical generated. Why four?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: @TupleConstructor made one for each length of the property list: three, two, one and none

    @Canonical includes @TupleConstructor, whose constructors take the properties in order, each from the right optional.

Interview questions

Warm-up (fresher to mid-level): What does == do in Groovy, and how do you test whether two variables hold the same object?

In Groovy, == compares values. For a type that implements Comparable it calls compareTo, and for other types equals, so two strings with the same characters are == even when they are different objects, and null == null is true without a NullPointerException. That is the opposite of Java, where == on objects tests identity. In Groovy, identity is a.is(b), or a === b since Groovy 3. One consequence to remember: BigDecimal 1.0 and 1.00 are == in Groovy, because compareTo ignores the scale, but not equals, which compares the scale too, so a HashSet can hold both. In the lesson, 1.0 == 1.00 printed true, equals printed false, and is on two equal strings printed false.

Two longer questions about ports from Java:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A Groovy report shows 30 minutes for a docs build that the CI server says took 0 minutes. The code reads def minutes = build?.minutes ?: 30. Explain the bug, fix it, and name the other values the same line mishandles.

    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 billing summary was ported from Java to Groovy line by line. The Java version printed 11 as the minutes per run where the Groovy one prints 11.25, and a check that amount / 3 * 3 equals the amount now fails. Explain both, and fix the port.

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What do builds.sum { it.minutes } and builds.max() return when builds is empty?

    null for both, where Java's IntStream.sum() gives 0 and max() an empty Optional. builds.sum(0) { it.minutes } starts at 0, so it returns 0 for no items.

  2. What does == compare in Groovy, and how do you ask for identity?

    Values: compareTo for Comparable types and equals for others, so two equal strings built in different ways are ==. Identity is a.is(b), or a === b from Groovy 3 on.

  3. Are the BigDecimal values 1.0 and 1.00 equal in Groovy?

    == says yes, because it goes through compareTo, which ignores the scale; equals says no, because the scales differ, so a HashSet can hold both.

  4. Which values does Groovy truth treat as false?

    null, empty strings, collections and maps, the number zero and a matcher without a match. The text 'false', the text '0' and a map holding one entry are all true.

  5. build?.minutes ?: 30 is applied to a 0-minute build. Why is the result 30?

    ?: falls back whenever its left side is false under Groovy truth, and 0 is false. Where 0 can be a real value, test for null instead: m != null ? m : 30.

  6. What is 7 / 2 in Groovy, and how do you divide whole numbers the way Java does?

    3.5, a BigDecimal, and an endless division keeps 10 decimals, so 2 / 3 * 3 is 2.0000000001. 7.intdiv(2) is 3 and (-7).intdiv(2) is −3, as in Java.

  7. What does a failing Groovy power assertion print?

    The expression, with the value of every evaluated part written under it. A part that never ran, such as the right side of a false &&, shows nothing.

  8. Which members does @Canonical write into a Groovy class?

    It stands for @ToString, @EqualsAndHashCode and @TupleConstructor: constructors from all properties down to none, plus toString, equals, hashCode and canEqual.

Key takeaways

  • Most Java compiles as Groovy; a port gets shorter with def, collection literals, closures and GStrings, and it prints maps and @Canonical classes in Groovy’s own format.
  • The GDK methods take closures, it is a closure’s implicit parameter and *. reads a property from every item; sum() and max() of an empty list return null, so give sum a start value.
  • == compares values through compareTo or equals, so BigDecimal 1.0 and 1.00 are ==; is() and === test identity.
  • Groovy truth makes null, zero and empty values false, so ?: replaces a real 0; test for null when 0 can be a value.
  • / gives a BigDecimal (3.5 for 7 / 2, and 2.0000000001 for 2 / 3 * 3), intdiv divides as Java does, round() sends −2.5 to −3, and int arithmetic still wraps.
  • A power assertion prints every evaluated part of a failing expression, and @Canonical generates the constructors, equals, hashCode and toString of a data class.

Terms in this lesson

AST transformation
Code that changes a program while the compiler builds it, switched on by an annotation such as @Canonical, which adds constructors, equals, hashCode and toString to a class.
Elvis operator
Groovy's a ?: b, which gives a when a is true under Groovy truth and b otherwise. It replaces null, but also 0, empty text and false, so it is no test for null alone.
Groovy truth
The rules by which any Groovy value counts as true or false in a condition: null, zero, empty strings, empty collections and maps and a matcher without a match are false; non-empty text, even 'false', is true.
GString
A double-quoted Groovy string with placeholders, $name or ${expression}, whose values are filled in when the string is used. A double-quoted string without placeholders, and any single-quoted string, is a plain java.lang.String.
optional typing
Writing a type only where it helps, and def where it does not. In Groovy def is the same as Object, and a call on such a variable finds its method while the program runs.
power assertion
Groovy's assert statement, which is always on: when its condition is false it prints the expression with the value of every part that was evaluated written under it.
safe navigation operator
Groovy's ?. operator: a?.b reads b from a, or gives null when a is null instead of throwing a NullPointerException.
spread operator
Groovy's *. operator: list*.name reads name from every item of the list and collects the results in a new list. An item that is null gives null instead of an exception.

References

Related tools

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.