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.
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:
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:
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
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:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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]].
- 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:
// 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 }}" 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
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:
// 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]}" 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
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':
// 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'}"
} 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:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- api, 12 minutes. build?.minutes reads 12. A non-zero number is true under Groovy truth, so ?: keeps it: api gives 12 either way.
- 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.
- 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.
- No build at all. build itself is null, so ?. returns null instead of throwing a NullPointerException, and ?: gives 30 again.
- 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 ?:
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:
// 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}" 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.
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
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:
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
} 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
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.
- Print values the way Groovy's println shows them To do
- 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.
Noneisnull, andTrueandFalsearetrueandfalse.- 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:valuepairs 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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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:
buildis a dict such as{"name": "docs", "minutes": 0}, orNonewhen there is no build;- return
build["minutes"]when the build exists and has minutes that are notNone, also when they are 0; - return
defaultotherwise: no build, nominuteskey, orminutesset toNone.
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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
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.
-
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.
-
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.
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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@Canonicalclasses in Groovy’s own format. - The GDK methods take closures,
itis a closure’s implicit parameter and*.reads a property from every item;sum()andmax()of an empty list returnnull, so givesuma start value. ==compares values throughcompareToorequals, soBigDecimal1.0 and 1.00 are==;is()and===test identity.- Groovy truth makes
null, zero and empty values false, so?:replaces a real 0; test fornullwhen 0 can be a value. /gives aBigDecimal(3.5 for 7 / 2, and 2.0000000001 for 2 / 3 * 3),intdivdivides as Java does,round()sends −2.5 to −3, andintarithmetic still wraps.- A power assertion prints every evaluated part of a failing expression, and
@Canonicalgenerates the constructors,equals,hashCodeandtoStringof 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
- Differences with Java (The Apache Software Foundation)
- Groovy syntax: numbers, strings and the division operator (The Apache Software Foundation)
- Groovy operators: safe navigation, Elvis, spread and identity (The Apache Software Foundation)
- Groovy semantics: optional typing and Groovy truth (The Apache Software Foundation)
- Closures (The Apache Software Foundation)
- Testing guide: power assertions (The Apache Software Foundation)
- Metaprogramming: code generation transformations (@Canonical) (The Apache Software Foundation)
- Groovy JDK: java.math.BigDecimal (round) (The Apache Software Foundation)
- Java SE 21 API: java.lang.Math (round, floorDiv, addExact) (Oracle)
- Java SE 21 API: java.math.BigDecimal (equals and compareTo) (Oracle)
- Java SE 21 API: java.util.stream.IntStream (sum and max) (Oracle)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress