Working with Collections
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Groovy 6.0 (6.0.0, 2026-09-22) and every example was run with Groovy 6.0.0 on JDK 26
(checked on 2026-10-14). Groovy keeps the JDK collection classes (ArrayList, LinkedHashMap, HashSet) and adds to them
the methods that make collection code short: each, collect, findAll, inject, groupBy, sort and several hundred more.
Almost all of them take a closure (see Closures) and, since 6.0, also the
java.util.function interfaces. The literal syntax, indexing and operators for lists, maps and ranges are on
Lists & Arrays, Maps and
Ranges, and SQL-like queries over collections are on
GINQ: SQL-like Collection Queries. The Java side is
described on Java Collections Framework and
Java Streams & Collectors, and the Kotlin counterpart on
Kotlin Collections & Sequences.
All the examples work on the same data, the readings of a small greenhouse: nine measurements from three zones, each a
temperature or a humidity value taken at a given hour. Every snippet below is a fragment that uses the readings list
declared once here (a record, see Records & Sealed Types); the
output shown is what that snippet printed when it was run after this setup.
record Reading(String zone, String kind, double value, int hour) { }
def readings = [
new Reading('north', 'temp', 21.5, 6), new Reading('north', 'temp', 24.0, 9),
new Reading('north', 'humidity', 61.0, 6), new Reading('south', 'temp', 28.5, 6),
new Reading('south', 'temp', 31.2, 9), new Reading('south', 'humidity', 48.0, 9),
new Reading('east', 'temp', 19.0, 6), new Reading('east', 'temp', 22.4, 9),
new Reading('east', 'humidity', 70.5, 9)
]
Two habits save time on this page. First, always write println(expr) with parentheses when the expression starts with
[: println [1, 2].size() is parsed as an index into println and fails with a MissingPropertyException. Second, check
whether a method mutates its receiver (sort, unique, reverse in some forms) or returns a new collection (toSorted,
toUnique, findAll); the GDK uses both styles and the names do not always say which.
The shape of the pipeline
The most useful way to read GDK collection code is as a pipeline in which each step takes a collection and returns a new one:
filter, reshape, group and summarise. The diagram shows the pipeline built later on this page: the temperature readings are
selected with findAll, reduced to the fields that matter with collect, grouped by zone with groupBy, and finally
turned into a map of averages with collectEntries.
Iterating
each calls a closure for every element; eachWithIndex adds the position, reverseEach walks a list backwards, and
indexed() turns a list into a map of index to element (with an optional starting index) so the position can be used with
the other methods. On a map, a closure with two parameters receives the key and value, and one with a single parameter
receives the Map.Entry.
readings.each { print "${it.zone} " }
println()
readings.eachWithIndex { r, i -> if (i < 3) print "$i:${r.value} " }
println()
readings.reverseEach { print "${it.hour} " }
println()
readings.indexed(1).each { i, r -> if (i <= 2) print "$i->${r.zone} " }
println()
[a: 1, b: 2].each { k, v -> print "$k=$v " }
[a: 1, b: 2].eachWithIndex { e, i -> print "$i:${e.key} " }
println()
// each cannot break; use find, any, every or a for loop to stop early
println readings.find { it.value > 25 }
for (r in readings) { if (r.value > 25) { println "stop at ${r.zone}"; break } }
north north north south south south east east east
0:21.5 1:24.0 2:61.0
9 9 6 9 9 6 6 9 6
1->north 2->north
a=1 b=2 0:a 1:b
Reading[zone=north, kind=humidity, value=61.0, hour=6]
stop at north
each returns the receiver and cannot be interrupted with break (a return inside the closure only ends that one
iteration, see Closures). To stop early use find, any, every, takeWhile
or a for loop, as the last lines show. For iteration over ranges, times, upto and step, see
Control Structures.
Filtering and searching
find returns the first matching element (or null), findAll all of them, and findResult/findResults combine
filtering with transformation by keeping the non-null results of the closure. any, every and count answer yes/no and
how-many questions, max and min accept a closure (a one-parameter closure is a key extractor, a two-parameter one a
comparator), and findIndexOf, findLastIndexOf and findIndexValues return positions.
println readings.find { it.kind == 'humidity' }
println readings.findAll { it.kind == 'temp' && it.value > 24 }*.value
println readings.findIndexOf { it.zone == 'south' }
println readings.findLastIndexOf { it.zone == 'south' }
println readings.findIndexValues { it.hour == 6 }
println readings.any { it.value > 30 }
println readings.every { it.value > 30 }
println readings.count { it.zone == 'east' }
println readings.max { it.value }.zone
println readings.min { it.value }.zone
println readings*.value.max()
println readings.find { it.value > 100 } // no match gives null
println readings.findResult { it.value > 30 ? it.zone : null }
println readings.findResults { it.kind == 'humidity' ? it.value : null }
Reading[zone=north, kind=humidity, value=61.0, hour=6]
[28.5, 31.2]
3
5
[0, 2, 3, 6]
true
false
3
east
east
70.5
null
north
[61.0, 48.0, 70.5]
grep filters with any object that has an isCase method, which is the same protocol as switch: a class, a regular
expression, a range, a collection or a closure. split divides a collection into the matching and non-matching parts, and
take, drop, takeWhile, dropWhile, first, last, head, tail and init slice a list without index arithmetic.
def temps = readings.findAll { it.kind == 'temp' }*.value
println temps
println(temps.grep(19..23)) // an int range never contains a Double
println(temps.grep { it >= 19 && it < 23 }) // any closure works as the filter
println(['n1', 's2', 'e3', 'x'].grep(~/[nse]\d/))
println([1, 'a', 2.5, null, 3].grep(Number))
println([1, 'a', null, 0, ''].grep()) // no argument: keep the Groovy-true elements
println temps.split { it > 22 } // two lists: matching and the rest
println temps.take(2)
println temps.drop(5)
println temps.takeWhile { it < 30 }
println temps.dropWhile { it < 30 }
println temps.takeRight(2)
println([temps.first(), temps.last(), temps.head(), temps.tail().size(), temps.init().size()])
println temps[2..4]
println temps[-2..-1]
println temps.getAt([0, 2, 4])
println(temps.indexOf(31.2d))
println(temps.contains(19.0d))
println(temps.containsAll([19.0d, 24.0d]))
println(temps.contains(19.0)) // a BigDecimal literal is not equal to a Double
println temps.sum()
println temps.disjoint([1.0, 2.0])
[21.5, 24.0, 28.5, 31.2, 19.0, 22.4]
[]
[21.5, 19.0, 22.4]
[n1, s2, e3]
[1, 2.5, 3]
[1, a]
[[24.0, 28.5, 31.2, 22.4], [21.5, 19.0]]
[21.5, 24.0]
[22.4]
[21.5, 24.0, 28.5]
[31.2, 19.0, 22.4]
[19.0, 22.4]
[21.5, 22.4, 21.5, 5, 5]
[28.5, 31.2, 19.0]
[19.0, 22.4]
[21.5, 28.5, 19.0]
3
true
true
false
146.6
true
Two pitfalls appear in this output. An int range such as 19..23 does not contain a Double such as 21.5, so grep(19..23)
returns an empty list: use a closure with comparisons for decimals (a decimal range steps by one and is rarely what you want,
see Ranges). And contains and indexOf use equals, so the BigDecimal
literal 19.0 is not equal to the Double 19.0 (19.0d); this is the number-type behaviour described on
Numbers & Numeric Literals.
Transforming
collect maps every element through a closure and returns a list; with an initial collection as the first argument it
collects into that instead (a Set, a TreeMap). collectMany maps and flattens, collectEntries builds a map from pairs, flatten
flattens nested lists, and withIndex()/indexed() pair each element with its position. The spread operator *. is shorthand
for a collect on a property or method, and +, - and intersect combine lists.
println readings.collect { it.value }.take(3)
println readings*.zone.unique() // spread operator is a collect on a property
println readings.collect { [it.zone, it.value] }.take(2)
println readings.collectMany { it.kind == 'temp' ? [it.zone] * 2 : [] }.take(4)
println readings.collectEntries { [(it.zone + it.hour + it.kind[0]): it.value] }.take(3)
println readings.take(3).collectEntries { r -> [r.zone, r.value] }
println readings.take(3).collect(new HashSet()) { it.zone }
println readings.take(2).collectEntries(new TreeMap()) { [it.value, it.zone] }
println([[1, [2, 3]], [4]].flatten())
println([[1, 2], [3]].sum()) // concatenation by plus
println(['a', 'b', 'c'].withIndex())
println(['a', 'b', 'c'].indexed())
println(['a', 'b', 'c'].indexed(10))
println(['a', 'b'] + ['c'] - ['a'])
println([1, 2, 3, 4] - [2, 3])
println([1, 2, 3].intersect([2, 3, 4]))
println([3, 1, 2].reverse())
println([1, 2, 3].join(' | '))
println([[1, 2], [3, 4]].collect { a, b -> a + b })
println([1, 2, 3].collect { it * 2 } as Set)
[21.5, 24.0, 61.0]
[north, south, east]
[[north, 21.5], [north, 24.0]]
[north, north, north, north]
[north6t:21.5, north9t:24.0, north6h:61.0]
[north:61.0]
[north]
[21.5:north, 24.0:north]
[1, 2, 3, 4]
[1, 2, 3]
[[a, 0], [b, 1], [c, 2]]
[0:a, 1:b, 2:c]
[10:a, 11:b, 12:c]
[b, c]
[1, 4]
[2, 3]
[2, 1, 3]
1 | 2 | 3
[3, 7]
[2, 4, 6]
collectEntries accepts a closure returning a two-element list [key, value] or a single-entry map [(key): value]; later
entries replace earlier ones with the same key. Because it returns a LinkedHashMap, insertion order is kept. Like
collect, it also takes a target map as its first argument.
Aggregating
inject is Groovy’s fold: it threads an accumulator through the elements, starting from the seed you give it or, without a
seed, from the first element. sum is the common special case (with an optional closure and an optional initial value;
an empty collection sums to null unless you pass a seed), max/min pick the extreme, and average computes the mean of numbers.
countBy counts elements per key, groupBy collects them per key and accepts several closures for nested grouping, and
injectAll (5.0) is like inject but returns every intermediate result.
def temps = readings.findAll { it.kind == 'temp' }
println temps.inject(0) { acc, r -> acc + r.value }
println temps.inject { a, b -> a.value > b.value ? a : b }.zone // no seed: first element is the seed
println temps*.value.sum()
println temps.sum { it.value }
println temps.sum { it.value } / temps.size()
println readings.sum(0) { it.hour }
println([].sum())
println([].sum(0))
println temps*.value.max()
println temps.max { it.value }.zone
println readings.countBy { it.zone }
println readings.countBy { it.kind }
println readings.groupBy { it.zone }.collectEntries { zone, rs -> [zone, rs.size()] }
println readings.groupBy({ it.zone }, { it.kind }).collectEntries { z, byKind -> [z, byKind.keySet()] }
println readings.groupBy({ it.zone }, { it.kind }).north.temp*.value
println temps*.value.injectAll(0) { a, b -> a + b }
println([3, 1, 2].inject([]) { acc, n -> acc << n * 10 })
println readings.groupBy { it.hour }.collectEntries { h, rs -> [h, rs*.value.sum()] }
println temps.collect { it.value }.average()
146.6
south
146.6
146.6
24.433333333333334
69
null
0
31.2
south
[north:3, south:3, east:3]
[temp:6, humidity:3]
[north:3, south:3, east:3]
[north:[temp, humidity], south:[temp, humidity], east:[temp, humidity]]
[21.5, 24.0]
[21.5, 45.5, 74.0, 105.2, 124.2, 146.6]
[30, 10, 20]
[6:130.0, 9:196.1]
24.433333333333334
Note that [].sum() is null while [].sum(0) is 0, so pass a seed when the collection can be empty. injectAll returns the running
totals, and groupBy with two closures returns a map of maps. A Map is not Iterable, so sum does not exist on it; use
values().sum() (a later example shows which methods maps do have).
groupByMany (6.0)
groupByMany is groupBy for items that belong to several groups: the closure returns a list of keys and the item is added to the
group of each. It fits many-to-many data such as tags, and on a map whose values are lists the no-argument form inverts
the relation (GROOVY-11808).
println(['ant', 'bee', 'ape', 'cow', 'pig'].groupByMany { w -> w.toSet().intersect('aeiou'.toSet()) })
println(readings.groupByMany { [it.zone, it.kind] }.collectEntries { k, rs -> [k, rs.size()] }.sort())
println([north: ['frost', 'wind'], south: ['wind'], east: []].groupByMany())
[a:[ant, ape], e:[bee, ape], i:[pig], o:[cow]]
[east:3, humidity:3, north:3, south:3, temp:6]
[frost:[north], wind:[north, south]]
Sorting
sort() sorts a list in place and also returns it; sort(false) and toSorted() leave the receiver alone and return a new
list. A one-parameter closure is a key (sort by -it.value for descending), a two-parameter closure or a Comparator is a
comparison; the spaceship operator <=> and the Elvis operator chain several keys, and Comparator.comparing(...) works too.
Since 6.0 isSorted() checks order without sorting (GROOVY-11891), with natural ordering, a Comparator or a closure.
def temps = readings.findAll { it.kind == 'temp' }
def values = temps*.value
def copy = values.toSorted() // new list, receiver untouched (GDK 2.4+)
println(copy)
println(values)
def same = values.sort() // sorts values in place AND returns it
println(same.is(values))
println(values)
println(temps*.value.sort(false)) // sort(false) returns a sorted copy
println(temps.sort { -it.value }*.zone) // a closure of one parameter is a key
println(temps.toSorted { a, b -> a.zone <=> b.zone ?: b.value <=> a.value }.collect { "$it.zone $it.value" })
println(temps.toSorted(Comparator.comparing { Reading r -> r.zone }.thenComparing { Reading r -> r.value }).collect { "$it.zone $it.value" })
println(readings.sort(false) { it.hour }*.hour)
println(['bb', 'a', 'ccc'].sort { it.size() })
println(['bb', 'a', 'ccc'].toSorted(Comparator.reverseOrder()))
println(values.isSorted())
println(values.isSorted { a, b -> b <=> a })
println([3, 1, 2].isSorted())
println([1, 2, 2, 3].isSorted())
println(['a', 'B', 'c'].toSorted(String.CASE_INSENSITIVE_ORDER))
println([3, 1, 2].max())
println(['kiwi', 'fig', 'apple'].max { it.size() })
println([a: 3, b: 1].sort { it.value })
[19.0, 21.5, 22.4, 24.0, 28.5, 31.2]
[21.5, 24.0, 28.5, 31.2, 19.0, 22.4]
true
[19.0, 21.5, 22.4, 24.0, 28.5, 31.2]
[19.0, 21.5, 22.4, 24.0, 28.5, 31.2]
[south, south, north, east, north, east]
[east 22.4, east 19.0, north 24.0, north 21.5, south 31.2, south 28.5]
[east 19.0, east 22.4, north 21.5, north 24.0, south 28.5, south 31.2]
[6, 6, 6, 6, 9, 9, 9, 9, 9]
[a, bb, ccc]
[ccc, bb, a]
true
false
false
true
[a, B, c]
3
apple
[b:1, a:3]
The first line shows toSorted() leaving values unchanged and the following lines show sort() changing it and returning the
very same list (same.is(values) is true). Sorting a Map with sort { } returns a new LinkedHashMap ordered by the closure.
Unique, chunks, combinations and zips
This group reshapes sequences. unique() removes duplicates in place, toUnique() returns a new list, and both accept a
closure that defines "the same". collate(n) splits a list into chunks of size n (with a step for overlapping windows and a
flag to drop a short tail), chop(sizes...) splits at explicit sizes, transpose() turns rows into columns, and zip/zipAll/interleave
(5.0) combine two lists element by element. combinations() is the cartesian product of a list of lists, permutations() all orderings,
subsequences() all non-empty ordered subsets, and tails()/inits() all suffixes and prefixes.
println([1, 2, 2, 3, 1].unique()) // unique() mutates the list in place
def zones = readings*.zone
println(zones.toUnique()) // toUnique() returns a new list
println(['a', 'B', 'A', 'b'].toUnique { it.toLowerCase() })
println((1..7).collate(3))
println((1..7).collate(3, false)) // do not keep the short last chunk
println((1..7).collate(3, 2)) // step 2: overlapping windows
println((1..7).chop(2, 3)) // chop: explicit chunk sizes
println([[1, 2], [3, 4], [5, 6]].transpose())
println([1, 2, 3].zip(['a', 'b', 'c']))
println([1, 2, 3].zip(['a', 'b']))
println([1, 2, 3].zipAll(['a'], 0, '-'))
println([1, 2, 3].interleave(['a', 'b', 'c']))
println([1, 2].combinations()) // needs a list of lists, see below
println([[1, 2], ['a', 'b']].combinations())
println([['n', 's'], [6, 9]].combinations { z, h -> "$z$h" })
println([1, 2, 3].permutations().toList().sort())
println([1, 2, 3].permutations { it.join() }.toList().sort())
println([1, 2, 3, 4].subsequences().size())
println([1, 2, 3].tails())
println([1, 2, 3].inits())
println(['x'].repeat(3))
println([1, 2] * 2)
[1, 2, 3]
[north, south, east]
[a, B]
[[1, 2, 3], [4, 5, 6], [7]]
[[1, 2, 3], [4, 5, 6]]
[[1, 2, 3], [3, 4, 5], [5, 6, 7], [7]]
[[1, 2], [3, 4, 5]]
[[1, 3, 5], [2, 4, 6]]
[[1, a], [2, b], [3, c]]
[[1, a], [2, b]]
[[1, a], [2, -], [3, -]]
[1, a, 2, b, 3, c]
[[1, 2]]
[[1, a], [2, a], [1, b], [2, b]]
[n6, s6, n9, s9]
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
[123, 132, 213, 231, 312, 321]
15
[[1, 2, 3], [2, 3], [3], []]
[[1, 2, 3], [1, 2], [1], []]
[x, x, x]
[1, 2, 1, 2]
permutations() returns a Set of lists and the number of results grows factorially, so the call is only suitable for
small inputs. eachPermutation and eachCombination visit the results without building the whole collection.
New in 6.0: neighbours, runs and groupings
Groovy 6.0 adds zipWithNext and groupConsecutive (GROOVY-12016) for questions about neighbouring elements. zipWithNext
pairs each element with its successor, optionally combining the pair with a closure, which is the idiom for deltas.
groupConsecutive splits an iterable into maximal runs, by equality, by a key or by an adjacency predicate; unlike groupBy,
only consecutive equal keys group together. Both have lazy Iterator variants and array overloads. isSorted() was shown above and
groupByMany earlier.
println([21.5, 24.0, 28.5].isSorted())
println([21.5, 28.5, 24.0].isSorted())
println([10, 13, 12, 18].zipWithNext())
println([10, 13, 12, 18].zipWithNext { a, b -> b - a })
println([1, 1, 2, 2, 2, 3, 1].groupConsecutive())
println(readings.groupConsecutive { it.zone }.collect { run -> "${run[0].zone}:${run.size()}" })
println(readings*.hour.groupConsecutive { a, b -> b >= a }.size())
true
false
[[10, 13], [13, 12], [12, 18]]
[3, -1, 6]
[[1, 1], [2, 2, 2], [3], [1]]
[north:3, south:3, east:3]
3
Lazy iterators
The collection methods above build a new list at each step. For long or unbounded sequences the Iterator receiver has
lazy variants: collecting, findingAll, findingResults, grepping (6.0), collate, takeWhile, drop, unique,
withIndex and others return another Iterator that computes elements only when the consumer asks for them, like the
intermediate operations of a Java stream. A terminal call (toList(), take(n).toList(), each, sum) runs the pipeline.
Groovy 5.0 widened this set considerably.
def temps = readings.findAll { it.kind == 'temp' }.iterator()
def lazy = temps.collecting { it.value }.findingAll { it > 22 } // nothing evaluated yet
println(lazy instanceof Iterator)
println(lazy.toList())
// an infinite sequence: only lazy methods can be applied before limiting it
def evens = (1..Integer.MAX_VALUE).iterator().findingAll { it % 2 == 0 }
println(evens.take(3).toList())
def pairs = [1, 3, 6, 10, 15].iterator().collate(2)
println(pairs.toList())
println(['row'].repeat().take(3).join('-'))
def seen = []
def mapped = (1..5).iterator().collecting { seen << it; it * it }
println(seen) // empty: still lazy
println(mapped.take(2).toList())
println(seen) // only two elements were pulled
println([1, 0, null, 2].iterator().grepping().toList())
true
[24.0, 28.5, 31.2, 22.4]
[2, 4, 6]
[[1, 3], [6, 10], [15]]
row-row-row
[]
[1, 4]
[1, 2]
[1, 2]
seen stays empty until the pipeline is consumed and receives only the two elements that take(2) pulled, which is the laziness
at work. A lazy iterator is single use: once consumed it cannot be replayed, so call toList() if the result is needed more
than once.
Streams interoperability
The JDK streams API is available as usual, and Groovy makes it more convenient: a closure is coerced to the functional
interface a stream method expects (Predicate, Function, ToDoubleFunction), Stream.toList() is the JDK’s, and 6.0 adds
toMutableList(), toImmutableList() and toImmutableSet() as explicit-mutability accumulators for streams and iterables
(GROOVY-12037). Where a method needs a specific functional type that Groovy cannot infer from the context, add as Function
(or the matching interface), as in the groupingBy call below.
import java.util.stream.Collectors
def temps = readings.findAll { it.kind == 'temp' }
println(temps.stream().mapToDouble { it.value }.max().asDouble)
println(temps.stream().filter { it.zone == 'north' }.map { it.value }.toList()) // JDK 16+ Stream.toList()
println(temps.stream().collect(Collectors.groupingBy({ it.zone } as java.util.function.Function, Collectors.counting())).sort())
println(temps.stream().map { it.zone }.distinct().collect(Collectors.joining(',', '[', ']')))
def mutable = temps.stream().map { it.value }.toMutableList() // 6.0
mutable << 99.9d
println(mutable.size())
def fixed = temps.stream().map { it.value }.toImmutableList()
try { fixed << 99.9d } catch (UnsupportedOperationException e) { println 'immutable' }
println(temps.stream().map { it.zone }.toImmutableSet().size())
def asList = (1..3).stream().collect(Collectors.toList())
println(asList)
println(Arrays.asList(3, 1, 2).stream().sorted().toList())
println(temps.collect { it.value }.sum()) // the same without streams
println((1..4).stream().mapToInt { it }.sum())
println([1, 2, 3].stream().map { it * 2 }.collect(Collectors.toList()))
println(temps.parallelStream().mapToDouble { it.value }.sum())
31.2
[21.5, 24.0]
[east:2, north:2, south:2]
[north,south,east]
7
immutable
3
[1, 2, 3]
[1, 2, 3]
146.6
10
[2, 4, 6]
146.6
Most stream pipelines can be written with the GDK methods directly, with less ceremony, and collect, findAll, groupBy
and friends now also have overloads for java.util.function types that avoid the per-call closure allocation (see
Functional Programming). Reach for streams when you need
their parallelism, a Collector that has no GDK equivalent, or an API that returns a Stream.
GPath over collections
GPath is the name for navigating nested data with property access. On a collection, a property access applies to every element and
returns a list, so readings.zone is the same as readings*.zone; *. additionally works for method calls and is
null-safe for the list itself. Chaining property accesses digs through nested maps and lists, and the result of a missing
property on the elements is an error, not an empty result.
println(readings*.zone.unique()) // spread-dot
println(readings.zone.unique()) // property on a list: same GPath, implicit spread
println(readings.findAll { it.kind == 'temp' }.value)
println(readings*.value.take(3))
println(readings.hour.sum())
println(readings*.toString().take(1))
try { readings.nonexistent } catch (MissingPropertyException e) { println e.class.simpleName }
def tree = [zone: [name: 'north', beds: [[id: 'b1', plants: 12], [id: 'b2', plants: 7]]]]
println(tree.zone.beds.id)
println(tree.zone.beds*.plants.sum())
println(tree.zone.beds.find { it.plants > 10 }.id)
println(tree.'zone'.'beds'[1].id)
println(tree.zone.beds.collect { it.plants }.max())
println(null*.size())
println([null, 'ab']*.size())
println([[1, 2], [3]]*.size())
println(tree.zone.beds.size())
println(tree.zone.beds*.id.join('/'))
[north, south, east]
[north, south, east]
[21.5, 24.0, 28.5, 31.2, 19.0, 22.4]
[21.5, 24.0, 61.0]
69
[Reading[zone=north, kind=temp, value=21.5, hour=6]]
MissingPropertyException
[b1, b2]
19
b1
b2
12
null
[null, 2]
[2, 1]
2
b1/b2
The same navigation is used with the data formats: see JSON and XML Parsing, GPath & Security.
Methods added in Groovy 5.0
Groovy 5.0 added about 350 extension methods, many of them for collections, iterators and arrays. The most useful ones:
repeat, zip/zipAll/interleave, withCollectedKeys/withCollectedValues, collectKeys/collectValues on maps,
flattenMany, injectAll, set operators (union, &, |, ^), partitionPoint for sorted data, and array versions of
chop, indexed, collect and others.
// Groovy 5.0 additions
println(['row'].repeat(3) + ['your', 'boat'])
println(['na'].repeat().take(5).join('-'))
println(['a', 'b', 'c'].zip([1, 2, 3]).collectEntries())
println(['Groovy', 'Java', 'Kotlin'].withCollectedKeys { it.take(1) })
println(['Groovy', 'Java', 'Kotlin'].withCollectedValues { it.size() })
println([Groovy: 6, Java: 4].collectKeys { it.toLowerCase() })
println([Groovy: 6, Java: 4].collectValues { it * 2 })
println(['1', '2', 'foo', '3'].flattenMany { it.isNumber() ? Optional.of(it.toInteger()) : Optional.empty() })
println(readings*.hour.injectAll(0) { acc, h -> acc + h })
println(['a', 'b'].interleave([1, 2, 3]))
println(['a', 'b'].interleave([1, 2, 3], true))
def a = [1, 2, 3] as Set
def b = [2, 3, 4] as Set
println(a.union(b)); println(a & b); println(a | b); println(a ^ b)
println([1, 3, 5, 7].partitionPoint { it < 4 })
println(['a', 'b', 'c'].indexed().collect { i, s -> "$i$s" })
println(([5, 6, 7, 8] as int[]).collect { it * 2 })
println(([5, 6, 7, 8] as int[]).chop(2, 2))
println(([5, 6, 7, 8] as int[]).indexed())
println((1..10).chop(3, 3, 4))
println(readings.collectEntries { r -> [(r.zone): r.value] }.keySet().sort())
[row, row, row, your, boat]
na-na-na-na-na
[a:1, b:2, c:3]
[G:Groovy, J:Java, K:Kotlin]
[Groovy:6, Java:4, Kotlin:6]
[groovy:6, java:4]
[Groovy:12, Java:8]
[1, 2, 3]
[6, 15, 21, 27, 36, 45, 51, 60, 69]
[a, 1, b, 2]
[a, 1, b, 2, 3]
[1, 2, 3, 4]
[2, 3]
[1, 2, 3, 4]
[1, 4]
2
[0a, 1b, 2c]
[10, 12, 14, 16]
[[5, 6], [7, 8]]
[0:5, 1:6, 2:7, 3:8]
[[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]
[east, north, south]
Putting it together: averages per zone
The pipeline from the diagram, written as a chain of GDK calls. The assertion states the result so the example documents
itself, and a second version shows the same query as a statically compiled method with explicit types, which is how the code
would look in a class that is compiled with @CompileStatic (see
Static Type Checking & @CompileStatic).
def averageTempByZone = readings
.findAll { it.kind == 'temp' } // 1. keep temperature readings
.collect { [zone: it.zone, celsius: it.value] } // 2. keep only what is needed
.groupBy { it.zone } // 3. zone -> list of readings
.collectEntries { zone, rs -> // 4. zone -> average
[zone, (rs*.celsius.sum() / rs.size()).round(2)]
}
println averageTempByZone
assert averageTempByZone == [north: 22.75, south: 29.85, east: 20.7]
// the same query as a typed, statically compiled method
@groovy.transform.CompileStatic
static Map<String, BigDecimal> averages(List<Reading> rs) {
rs.findAll { it.kind == 'temp' }
.groupBy { it.zone }
.collectEntries { String zone, List<Reading> zs ->
[zone, (zs.sum { it.value } as BigDecimal) / zs.size()]
} as Map<String, BigDecimal>
}
println averages(readings)
[north:22.75, south:29.85, east:20.7]
[north:22.75, south:29.85, east:20.7]
Chaining works because each call returns a new collection. The cost is one intermediate collection per step; for large data use the lazy iterator methods above, or a stream, so that the data is traversed once.
Copy or modify, and what a Map does differently
Most mistakes with collections come from not knowing whether a call mutates. The summary: sort, unique, reverse (with
a true flag), add, remove, removeAll, retainAll, removeIf and << modify the receiver; toSorted, toUnique, findAll,
collect, +, - and plus build a new collection. Mutating a collection while iterating it with each throws
ConcurrentModificationException for the JDK collections; use removeIf, an explicit Iterator, or build a new list. The copy
semantics and the immutability helpers are covered in more depth on
Lists & Arrays.
def temps = [21.5, 24.0, 19.0]
def sorted = temps.sort() // surprise: temps is now sorted too
println temps.is(sorted)
def original = [3, 1, 2]
def copy = original.toSorted()
println original
println copy
def m = [a: 1, b: 2, c: 3]
println m.findAll { it.value > 1 }
println m.findAll { it.value > 1 }.getClass().simpleName
println m.collect { it.value }
println m.collectEntries { k, v -> [v, k] }
println m.find { it.value > 1 }
println m.count { k, v -> v > 1 }
println m.groupBy { it.value % 2 }
println m.sort { -it.value }
println m.max { it.value }
println m.values().sum() // a Map is not Iterable: no sum, max-by-closure on entries only
println m.any { k, v -> v == 3 }
def list = [1, 2, 3, 4]
try {
list.each { if (it == 2) list.remove(it) }
} catch (ConcurrentModificationException e) {
println e.class.simpleName
}
println list // [1, 2, 4]: remove(2) removed the element at index 2
list.removeIf { it % 2 == 0 } // the JDK way: mutate through the API
println list
println list.findAll { it != 1 } // or build a new list
true
[3, 1, 2]
[1, 2, 3]
[b:2, c:3]
LinkedHashMap
[1, 2, 3]
[1:a, 2:b, 3:c]
b=2
2
[1:[a:1, c:3], 0:[b:2]]
[c:3, b:2, a:1]
c=3
6
true
ConcurrentModificationException
[1, 2, 4]
[1]
[]
Notes on the output:
-
sort()changestempsas well as returning it, sotemps.is(sorted)is true. -
On a map,
findAllreturns a map butcollectreturns a list;findreturns aMap.Entry;groupByreturns a map of maps;sortreturns a new ordered map. See Maps. -
The
ConcurrentModificationExceptionis thrown andlist.remove(it)is a trap in itself: for anIntegerargument Groovy callsremove(int index), notremove(Object), so the list lost its third element (3), not the2that was meant. The result[1, 2, 4]is printed after the failure for that reason.
Where the book is outdated
The Manning book Groovy in Action (publisher page in the references) devotes a chapter to collections, and its description
of each, collect, find, inject, sort and groupBy still holds. Check these points against the current documentation:
-
The collection methods grew a lot:
toSorted/toUnique(2.4), lazy iterator methods,zip,repeat,flattenMany,injectAlland the set operators (5.0),groupByMany,isSorted,zipWithNextandgroupConsecutive(6.0). -
The
java.util.functionoverloads (6.0), theStreamaccumulators andgroovyToStringformatting of collections did not exist when the book was written. -
The concurrent-modification and copy-versus-modify discussions apply unchanged, but modern code prefers
removeIfand the non-mutatingtoSortedandfindAll. -
Baselines: Groovy 6.0 needs JDK 17 or later.