Groovy adds dozens of methods to every collection, each taking a closure. Together they replace most loops — the Groovy equivalent of Java streams, but available directly on lists and maps.
The examples use this list of orders (maps, like parsed JSON):
def orders = [
[id: 'PO-1', customer: 'Acme', total: 250, status: 'SHIPPED'],
[id: 'PO-2', customer: 'Globex', total: 90, status: 'NEW'],
[id: 'PO-3', customer: 'Acme', total: 1200, status: 'NEW'],
[id: 'PO-4', customer: 'Initech', total: 40, status: 'CANCELLED'],
]
Iterate and transform
orders.each { println it.id }
orders.eachWithIndex { o, i -> println "$i -> ${o.id}" }
orders.collect { it.id } // [PO-1, PO-2, PO-3, PO-4]
orders*.customer.unique() // [Acme, Globex, Initech]
collect is Java’s map; *. is the shortcut for collecting a single property.
Filter and search
| Method | Returns | Java streams equivalent |
|---|---|---|
findAll { } | every match | filter(...).toList() |
find { } | first match or null | filter(...).findFirst() |
any { } | true if any match | anyMatch |
every { } | true if all match | allMatch |
count { } | how many match | filter(...).count() |
orders.findAll { it.status == 'NEW' }*.id // [PO-2, PO-3]
orders.find { it.total > 1000 }?.id // PO-3
orders.any { it.status == 'CANCELLED' } // true
Totals, extremes and sorting
orders.sum { it.total } // 1580
orders.max { it.total }.id // PO-3
orders.sort(false) { -it.total }*.id // biggest first, as a NEW list
Watch out: list.sort() sorts in place and changes the original list. Pass false as the first argument to get a sorted copy instead.
inject: fold into one value
def total = orders.inject(0) { acc, o -> acc + o.total }
inject (called reduce or fold elsewhere) starts with a value and combines each element into it.
Grouping and reshaping
orders.groupBy { it.customer } // [Acme:[...], Globex:[...], Initech:[...]]
orders.groupBy { it.customer }.collectEntries { k, v -> [k, v*.total.sum()] }
// [Acme:1450, Globex:90, Initech:40]
orders.countBy { it.status } // [SHIPPED:1, NEW:2, CANCELLED:1]
orders.collectEntries { [it.id, it.status] } // [PO-1:SHIPPED, ...]
def (big, small) = orders.split { it.total >= 100 } // two lists in one pass
Batching and flattening
(1..7).collate(3) // [[1, 2, 3], [4, 5, 6], [7]]
[[1, 2], [3, [4]]].flatten() // [1, 2, 3, 4]
lines.collectMany { l -> [l.sku] * l.qty }
collate is perfect for APIs with batch limits: send records 100 at a time with records.collate(100).each { batch -> send(batch) }.
The full example
// collection_methods.groovy — each, collect, findAll, find, inject, groupBy and friends.
def orders = [
[id: 'PO-1', customer: 'Acme', total: 250, status: 'SHIPPED'],
[id: 'PO-2', customer: 'Globex', total: 90, status: 'NEW'],
[id: 'PO-3', customer: 'Acme', total: 1200, status: 'NEW'],
[id: 'PO-4', customer: 'Initech', total: 40, status: 'CANCELLED'],
]
orders.each { println "${it.id}: ${it.total}" } // do something with each
orders.eachWithIndex { o, i -> println "$i -> ${o.id}" }
println orders.collect { it.id } // transform: [PO-1, PO-2, PO-3, PO-4]
println orders*.customer.unique() // spread + unique: [Acme, Globex, Initech]
println orders.findAll { it.status == 'NEW' }*.id // filter: [PO-2, PO-3]
println orders.find { it.total > 1000 }?.id // first match: PO-3
println orders.any { it.status == 'CANCELLED' } // true
println orders.every { it.total > 0 } // true
println orders.count { it.customer == 'Acme' } // 2
println orders.sum { it.total } // 1580
println orders.max { it.total }.id // PO-3
println orders.sort(false) { -it.total }*.id // sorted copy, biggest first
// inject: fold everything into one value (like reduce)
def total = orders.inject(0) { acc, o -> acc + o.total }
println "Total: $total"
// groupBy and countBy
def byCustomer = orders.groupBy { it.customer }
println byCustomer.collectEntries { k, v -> [k, v*.total.sum()] } // [Acme:1450, Globex:90, Initech:40]
println orders.countBy { it.status } // [SHIPPED:1, NEW:2, CANCELLED:1]
// collectEntries: build a map
println orders.collectEntries { [it.id, it.status] }
// split: two lists in one pass
def (big, small) = orders.split { it.total >= 100 }
println "big=${big*.id} small=${small*.id}"
// collate: chunk into batches (great for APIs with batch limits)
println((1..7).collate(3)) // [[1, 2, 3], [4, 5, 6], [7]]
// flatten and collectMany
println([[1, 2], [3, [4]]].flatten()) // [1, 2, 3, 4]
def lines = [[sku: 'AX', qty: 2], [sku: 'BX', qty: 1]]
println lines.collectMany { l -> [l.sku] * l.qty } // [AX, AX, BX]
Try it yourself
From orders, produce a report map from status to a comma-separated string of order IDs, e.g. [SHIPPED:'PO-1', NEW:'PO-2, PO-3', CANCELLED:'PO-4'].
Show a solution
def report = orders.groupBy { it.status }
.collectEntries { status, list -> [status, list*.id.join(', ')] }
println report
Key takeaways
collecttransforms,findAll/findfilter and search,any/every/counttest.sum,max,sort(false)andinjectsummarise.groupBy,countBy,collectEntriesandsplitreshape data in one line.collatebatches,flatten/collectManyflatten.