Skip to content
AgileLogics
7 min read

The Collection Toolkit

Prerequisites: Working with collections

The GDK adds dozens of methods to Java's collections. Knowing them turns most data-processing code into a short pipeline.

inject — reduce to one value

def nums = [4, 8, 15, 16, 23, 42]

println nums.inject(0) { acc, n -> acc + n }
println nums.inject { a, b -> a > b ? a : b }
println nums.sum { it * 2 }
println nums.average()
108
42
216
18

inject(initial) { accumulator, item -> ... } is Groovy's name for fold/reduce.

Grouping and counting

def words = ['apple', 'avocado', 'banana', 'blueberry', 'cherry', 'apple']

println words.countBy { it[0] }
println words.groupBy { it.size() }
println words.toUnique()
println words.split { it.startsWith('a') }
println words.collectMany { [it[0], it[-1]] }.toSet().sort()
[a:3, b:2, c:1]
[5:[apple, apple], 7:[avocado], 6:[banana, cherry], 9:[blueberry]]
[apple, avocado, banana, blueberry, cherry]
[[apple, avocado, apple], [banana, blueberry, cherry]]
[a, b, c, e, o, y]
  • countBy → map of key to count.
  • groupBy → map of key to list.
  • split → two lists: matching and non-matching.
  • collectMany → collect, then flatten one level.

Slicing and chunking

def list = (1..10).toList()

println list.take(3)
println list.drop(7)
println list.takeWhile { it < 4 }
println list.collate(4)             // chunks of 4
println list.collate(3, 1).take(3)  // sliding windows
println([[1, 2, 3], ['a', 'b', 'c']].transpose())
println([1, 2, 3].indexed())
[1, 2, 3]
[8, 9, 10]
[1, 2, 3]
[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10]]
[[1, 2, 3], [2, 3, 4], [3, 4, 5]]
[[1, a], [2, b], [3, c]]
[0:1, 1:2, 2:3]

collate(size, step) with a step smaller than the size gives overlapping windows; transpose pairs up items at the same position (a "zip").

Maps have a toolkit too

def scores = [ada: 91, linus: 78, grace: 88, alan: 65]

println scores.findAll { name, score -> score >= 80 }
println scores.collect { name, score -> "$name=$score" }.join(', ')
println scores.max { it.value }.key
println scores.sort { -it.value }.keySet()
println scores.subMap(['ada', 'alan'])
println scores.collectEntries { k, v -> [k.toUpperCase(), v >= 80] }
println scores.findResult { k, v -> v < 70 ? k : null }

def tally = [:].withDefault { 0 }
'mississippi'.each { tally[it]++ }
println tally
[ada:91, grace:88]
ada=91, linus=78, grace=88, alan=65
ada
[ada, grace, linus, alan]
[ada:91, alan:65]
[ADA:true, LINUS:false, GRACE:true, ALAN:false]
alan
[m:1, i:4, s:4, p:2]

withDefault creates a map that inserts a default value the first time a missing key is read — perfect for counters and grouping.

Combinations and set operations

def a = [1, 2, 3, 4]
def b = [3, 4, 5]

println a.intersect(b)
println a - b
println((a + b).unique())
println a.disjoint([7, 8])
println ([['S', 'M'], ['red', 'blue']].combinations())
println ([1, 2, 3].permutations().size())
println ([1, 2, 3].subsequences().size())
[3, 4]
[1, 2]
[1, 2, 3, 4, 5]
true
[[S, red], [M, red], [S, blue], [M, blue]]
6
7

Lazy pipelines with streams

def firstBigSquares = (1..Integer.MAX_VALUE).stream()
    .map { it * it }
    .filter { it % 7 == 0 }
    .limit(3)
    .toList()
println firstBigSquares
[49, 196, 441]

GDK methods like collect are eager — they build the whole result. Java streams (or Groovy's iterator()-based methods) are lazy, so they can work with huge or endless ranges.

Querying collections with GINQ

Groovy 4 added GINQ, an SQL-like query syntax for collections:

def people = [
    [name: 'Ada', dept: 'eng', salary: 120],
    [name: 'Grace', dept: 'eng', salary: 135],
    [name: 'Linus', dept: 'ops', salary: 98],
]

def rich = GQ {
    from p in people
    where p.salary > 100
    orderby p.salary in desc
    select p.name, p.salary
}
println rich.toList()
[[Grace, 135], [Ada, 120]]

GINQ also supports joins, groupby, and aggregates like count() and sum() — handy for reports over in-memory data.

Practice

Given a list of order maps with customer and amount, print each customer's total, highest first.

Show solution
def orders = [
    [customer: 'ada', amount: 30],
    [customer: 'bob', amount: 15],
    [customer: 'ada', amount: 20],
    [customer: 'cy', amount: 70],
]

orders.groupBy { it.customer }
      .collectEntries { name, list -> [name, list.sum { it.amount }] }
      .sort { -it.value }
      .each { name, total -> println "$name: $total" }
cy: 70
ada: 50
bob: 15