What you'll learn

  • each, eachWithIndex, collect and the spread operator
  • findAll, find, any, every, count
  • sum, max, sort and inject
  • groupBy, countBy, collectEntries, split and collate

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

MethodReturnsJava streams equivalent
findAll { }every matchfilter(...).toList()
find { }first match or nullfilter(...).findFirst()
any { }true if any matchanyMatch
every { }true if all matchallMatch
count { }how many matchfilter(...).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

  • collect transforms, findAll/find filter and search, any/every/count test.
  • sum, max, sort(false) and inject summarise.
  • groupBy, countBy, collectEntries and split reshape data in one line.
  • collate batches, flatten/collectMany flatten.