Learn Groovy higher-order functions. Groovy collect, inject, groupBy, collectEntries, and collectMany. 10+ tested examples with output on Groovy 5.x.
“Give me a collection and a closure, and I will give you back a transformed world.”
Abelson & Sussman, Structure and Interpretation of Computer Programs
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Intermediate | Reading Time: 17 minutes
The each loop is just the beginning – groovy collect, inject, groupBy, and their relatives form a rich set of higher-order functions that let you transform, aggregate, filter, and restructure collections with minimal code. If you already know about closures and the each loop, these methods are the natural next step.
In this post, we focus on three of the most powerful ones: groovy collect (transform every element), inject (reduce a collection to a single value), and groovy groupBy (partition elements into groups). We will also cover their close relatives like collectEntries, collectMany, countBy, and withDefault.
Every example is tested on Groovy 5.x with actual output. If you need to filter collections instead of transforming them, check out our Groovy findAll guide.
Table of Contents
What Are Higher-Order Functions?
A higher-order function is any function that does at least one of these two things:
- Takes a function (closure) as a parameter — like
collect,inject, andgroupBy - Returns a function (closure) as its result — like curried closures and method references
In Groovy, higher-order functions are everywhere. The GDK (Groovy Development Kit) adds dozens of them to Java’s collection classes. According to the official Groovy GDK documentation, these methods are what make Groovy collections so much more expressive than plain Java collections.
If you come from a Java background, think of these as the Groovy equivalents of Java Streams — except they have been part of Groovy since long before Java 8 existed, and they are often more concise.
Higher-Order Functions at a Glance
Here is a quick reference of the higher-order collection methods we will cover. Each one takes a closure and applies it to the collection in a different way.
| Method | Purpose | Returns |
|---|---|---|
collect | Transform each element | New list of transformed values |
collectEntries | Transform into key-value pairs | New map |
collectMany | Transform and flatten | New flat list |
inject | Reduce to single value (fold) | Accumulated result |
groupBy | Partition into groups | Map of key to list |
countBy | Count by criterion | Map of key to count |
sum | Sum with optional transform | Total value |
sort | Sort with custom comparator | Sorted list |
10 Practical Examples
Let us work through each function with real, tested code. We will build up from simple transformations to complex data processing pipelines.
Example 1: collect – Transform Every Element
The collect method is the Groovy equivalent of map() in other languages. It applies a closure to every element and returns a new list of results. The original collection is not modified.
Example 1: collect Basics
// Basic collect - transform numbers
def numbers = [1, 2, 3, 4, 5]
def doubled = numbers.collect { it * 2 }
println "Doubled: ${doubled}"
// Transform strings
def names = ["alice", "bob", "carol"]
def capitalized = names.collect { it.capitalize() }
println "Capitalized: ${capitalized}"
// Transform to different types
def lengths = names.collect { it.length() }
println "Lengths: ${lengths}"
// collect with index using collectWithIndex (Groovy 5.x)
def indexed = names.collect { "${it.toUpperCase()}" }
println "Uppercase: ${indexed}"
// Chaining collect with other methods
def result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.findAll { it % 2 == 0 } // filter evens
.collect { it * it } // square them
.collect { "Value: ${it}" } // format as string
println "Chained: ${result}"
// collect on a range
def squares = (1..5).collect { it ** 2 }
println "Squares: ${squares}"
// Original list is unchanged
println "Original: ${numbers}"
Output
Doubled: [2, 4, 6, 8, 10] Capitalized: [Alice, Bob, Carol] Lengths: [5, 3, 5] Uppercase: [ALICE, BOB, CAROL] Chained: [Value: 4, Value: 16, Value: 36, Value: 64, Value: 100] Squares: [1, 4, 9, 16, 25] Original: [1, 2, 3, 4, 5]
The key thing to remember: collect always returns a new list with the same number of elements as the input. If you need to filter and transform at the same time, chain findAll before collect.
Example 2: collect on Maps
When you call collect on a Map, the closure receives either a Map.Entry (single parameter) or key and value (two parameters). The result is always a list.
Example 2: collect on Maps
// collect on a Map with two parameters
def prices = [apple: 1.50, banana: 0.75, cherry: 3.00]
def formatted = prices.collect { fruit, price ->
"${fruit.capitalize()}: \$${String.format('%.2f', price)}"
}
println "Formatted: ${formatted}"
// Using single parameter (Map.Entry)
def entries = prices.collect { entry ->
"${entry.key} costs ${entry.value}"
}
println "Entries: ${entries}"
// Transform Map values with collectEntries (preserves Map structure)
def discounted = prices.collectEntries { fruit, price ->
[(fruit): price * 0.9]
}
println "10% off: ${discounted}"
// collect to create HTML options
def countries = [US: "United States", UK: "United Kingdom", CA: "Canada"]
def options = countries.collect { code, name ->
"<option value='${code}'>${name}</option>"
}
println "\nHTML options:"
options.each { println it }
Output
Formatted: [Apple: $1.50, Banana: $0.75, Cherry: $3.00] Entries: [apple costs 1.50, banana costs 0.75, cherry costs 3.00] 10% off: [apple:1.350, banana:0.675, cherry:2.700] HTML options: <option value='US'>United States</option> <option value='UK'>United Kingdom</option> <option value='CA'>Canada</option>
Notice the important difference: collect on a Map returns a list, while collectEntries returns a Map. If you want to transform a Map into another Map, use collectEntries. If you want to produce a list of values from a Map, use collect.
Example 3: collectEntries – Build Maps from Collections
The collectEntries method is incredibly useful when you need to convert a list into a Map or transform one Map into another. It expects the closure to return either a two-element list or a single-entry Map.
Example 3: collectEntries
// Convert a list to a Map
def names = ["Alice", "Bob", "Carol", "Dave"]
def nameLengths = names.collectEntries { name ->
[(name): name.length()]
}
println "Name lengths: ${nameLengths}"
// Using two-element list syntax
def nameUpper = names.collectEntries { [it, it.toUpperCase()] }
println "Name upper: ${nameUpper}"
// Build a lookup Map from objects
def people = [
[id: 1, name: "Alice", role: "Admin"],
[id: 2, name: "Bob", role: "User"],
[id: 3, name: "Carol", role: "Moderator"]
]
def byId = people.collectEntries { person ->
[(person.id): person]
}
println "\nLookup by ID:"
byId.each { id, person -> println " ${id} -> ${person.name} (${person.role})" }
// Invert a Map (swap keys and values)
def original = [a: 1, b: 2, c: 3]
def inverted = original.collectEntries { key, val -> [(val): key] }
println "\nInverted: ${inverted}"
// Build a frequency map from a list
def words = ["hello", "world", "hello", "groovy", "world", "hello"]
def freq = words.collectEntries { word ->
[(word): words.count(word)]
}
println "Frequency: ${freq}"
Output
Name lengths: [Alice:5, Bob:3, Carol:5, Dave:4] Name upper: [Alice:ALICE, Bob:BOB, Carol:CAROL, Dave:DAVE] Lookup by ID: 1 -> Alice (Admin) 2 -> Bob (User) 3 -> Carol (Moderator) Inverted: [1:a, 2:b, 3:c] Frequency: [hello:3, world:2, groovy:1]
The collectEntries method is one of the most versatile tools in Groovy. The closure must return either a Map (like [(key): value]) or a two-element List (like [key, value]). Note the parentheses around the key in [(key): value] — without them, Groovy treats it as a literal string key.
Example 4: inject – Reduce a Collection to a Single Value
The inject method is Groovy’s version of reduce or fold. It takes an initial value (the accumulator) and a closure that combines each element with the running total. By the time it has processed every element, you have a single result.
Example 4: inject (Reduce/Fold)
// Sum numbers with inject
def numbers = [1, 2, 3, 4, 5]
def sum = numbers.inject(0) { acc, num -> acc + num }
println "Sum: ${sum}"
// Product of numbers
def product = numbers.inject(1) { acc, num -> acc * num }
println "Product: ${product}"
// Concatenate strings
def words = ["Groovy", "is", "awesome"]
def sentence = words.inject("") { acc, word ->
acc.isEmpty() ? word : "${acc} ${word}"
}
println "Sentence: ${sentence}"
// Find the longest string
def names = ["Alice", "Bob", "Christopher", "Dave"]
def longest = names.inject("") { acc, name ->
name.length() > acc.length() ? name : acc
}
println "Longest name: ${longest}"
// Build a Map with inject
def pairs = [["a", 1], ["b", 2], ["c", 3]]
def map = pairs.inject([:]) { acc, pair ->
acc[pair[0]] = pair[1]
acc
}
println "Map from pairs: ${map}"
// inject without initial value (uses first element as initial)
def maxVal = [3, 7, 1, 9, 4].inject { acc, val -> acc > val ? acc : val }
println "Max value: ${maxVal}"
// Count characters across all strings
def totalChars = ["Hello", "World", "Groovy"].inject(0) { acc, str ->
acc + str.length()
}
println "Total characters: ${totalChars}"
Output
Sum: 15 Product: 120 Sentence: Groovy is awesome Longest name: Christopher Map from pairs: [a:1, b:2, c:3] Max value: 9 Total characters: 16
The inject method is extremely flexible because the accumulator can be any type — a number, a string, a list, a map, even a custom object. The key is to always return the accumulator from the closure. If you forget to return it, the next iteration gets null as the accumulator.
Example 5: groupBy – Partition Elements into Groups
The groupBy method partitions a collection into a Map where each key is the result of the closure and each value is a list of elements that produced that key. It is perfect for categorization tasks.
Example 5: groupBy
// Group numbers by even/odd
def numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
def evenOdd = numbers.groupBy { it % 2 == 0 ? "even" : "odd" }
println "Even/Odd: ${evenOdd}"
// Group strings by first letter
def names = ["Alice", "Anna", "Bob", "Brian", "Carol", "Charlie"]
def byLetter = names.groupBy { it[0] }
println "By first letter: ${byLetter}"
// Group by string length
def words = ["cat", "dog", "elephant", "ant", "bear", "fox", "giraffe"]
def byLength = words.groupBy { it.length() }
println "By length: ${byLength}"
// Group objects by property
def people = [
[name: "Alice", dept: "Engineering"],
[name: "Bob", dept: "Marketing"],
[name: "Carol", dept: "Engineering"],
[name: "Dave", dept: "Marketing"],
[name: "Eve", dept: "Engineering"]
]
def byDept = people.groupBy { it.dept }
println "\nBy department:"
byDept.each { dept, members ->
println " ${dept}: ${members.collect { it.name }}"
}
// Multi-level groupBy
def scores = [
[name: "Alice", subject: "Math", grade: "A"],
[name: "Bob", subject: "Math", grade: "B"],
[name: "Alice", subject: "Science", grade: "A"],
[name: "Bob", subject: "Science", grade: "A"],
[name: "Carol", subject: "Math", grade: "A"]
]
def bySubjectAndGrade = scores.groupBy({ it.subject }, { it.grade })
println "\nMulti-level groupBy:"
bySubjectAndGrade.each { subject, grades ->
println " ${subject}:"
grades.each { grade, students ->
println " ${grade}: ${students.collect { it.name }}"
}
}
Output
Even/Odd: [odd:[1, 3, 5, 7, 9], even:[2, 4, 6, 8, 10]]
By first letter: [A:[Alice, Anna], B:[Bob, Brian], C:[Carol, Charlie]]
By length: [3:[cat, dog, ant, fox], 8:[elephant], 4:[bear], 7:[giraffe]]
By department:
Engineering: [Alice, Carol, Eve]
Marketing: [Bob, Dave]
Multi-level groupBy:
Math:
A: [Alice, Carol]
B: [Bob]
Science:
A: [Alice, Bob]
The multi-level groupBy is particularly powerful. You can pass multiple closures, and Groovy will create nested Maps — one level of nesting per closure. This is great for building pivot-table-style data structures.
Example 6: collectMany – Transform and Flatten
The collectMany method is Groovy’s version of flatMap. It applies a closure to each element (the closure must return a collection), then flattens all the results into a single list. Use it when each element maps to multiple output values.
Example 6: collectMany (flatMap)
// Split sentences into words
def sentences = ["Hello World", "Groovy is great", "Java too"]
def allWords = sentences.collectMany { it.split(" ") as List }
println "All words: ${allWords}"
// Generate ranges from numbers
def seeds = [3, 5, 2]
def expanded = seeds.collectMany { n -> (1..n).toList() }
println "Expanded: ${expanded}"
// Extract tags from blog posts
def posts = [
[title: "Groovy Closures", tags: ["groovy", "functional", "closures"]],
[title: "Java Streams", tags: ["java", "streams", "functional"]],
[title: "Groovy Collections", tags: ["groovy", "collections"]]
]
def allTags = posts.collectMany { it.tags }.unique().sort()
println "All unique tags: ${allTags}"
// Versus collect (which gives nested lists)
def nested = posts.collect { it.tags }
println "\ncollect gives nested: ${nested}"
println "collectMany gives flat: ${posts.collectMany { it.tags }}"
// Practical: find all files in multiple directories (simulated)
def dirs = [
[path: "/src", files: ["Main.groovy", "Utils.groovy"]],
[path: "/test", files: ["MainTest.groovy"]],
[path: "/lib", files: ["dep1.jar", "dep2.jar", "dep3.jar"]]
]
def allFiles = dirs.collectMany { dir ->
dir.files.collect { file -> "${dir.path}/${file}" }
}
println "\nAll files:"
allFiles.each { println " ${it}" }
Output
All words: [Hello, World, Groovy, is, great, Java, too] Expanded: [1, 2, 3, 1, 2, 3, 4, 5, 1, 2] All unique tags: [closures, collections, functional, groovy, java, streams] collect gives nested: [[groovy, functional, closures], [java, streams, functional], [groovy, collections]] collectMany gives flat: [groovy, functional, closures, java, streams, functional, groovy, collections] All files: /src/Main.groovy /src/Utils.groovy /test/MainTest.groovy /lib/dep1.jar /lib/dep2.jar /lib/dep3.jar
The difference between collect and collectMany is important: collect returns a list of lists (nested), while collectMany flattens everything into a single list. Use collectMany whenever your transformation produces multiple values per input element.
Example 7: countBy – Count Elements by Criterion
The countBy method is like groupBy, but instead of collecting elements into groups, it counts how many elements fall into each group. It returns a Map of key to count.
Example 7: countBy
// Count words by length
def words = ["cat", "dog", "elephant", "ant", "bear", "fox", "giraffe", "bee"]
def byLength = words.countBy { it.length() }
println "Count by length: ${byLength}"
// Count characters in a string
def text = "hello world"
def charFreq = text.toList().countBy { it }
println "Character frequency: ${charFreq}"
// Count by category
def products = [
[name: "Laptop", category: "Electronics"],
[name: "Phone", category: "Electronics"],
[name: "Shirt", category: "Clothing"],
[name: "Tablet", category: "Electronics"],
[name: "Pants", category: "Clothing"],
[name: "Book", category: "Media"],
]
def categoryCount = products.countBy { it.category }
println "Products per category: ${categoryCount}"
// Count even vs odd in a range
def evenOddCount = (1..100).countBy { it % 2 == 0 ? "even" : "odd" }
println "1-100 even/odd: ${evenOddCount}"
// Compare with groupBy: countBy gives counts, groupBy gives lists
println "\ngroupBy: ${words.groupBy { it.length() }}"
println "countBy: ${words.countBy { it.length() }}"
Output
Count by length: [3:5, 8:1, 4:1, 7:1] Character frequency: [h:1, e:1, l:3, o:2, :1, w:1, r:1, d:1] Products per category: [Electronics:3, Clothing:2, Media:1] 1-100 even/odd: [odd:50, even:50] groupBy: [3:[cat, dog, ant, fox, bee], 8:[elephant], 4:[bear], 7:[giraffe]] countBy: [3:5, 8:1, 4:1, 7:1]
Use countBy when you only need the counts and not the actual elements. It is more memory-efficient than doing groupBy followed by collecting the sizes.
Example 8: sort – Custom Sorting with Closures
The sort method accepts a closure for custom comparison logic. You can provide either a single-parameter closure (sort by a computed key) or a two-parameter closure (custom comparator).
Example 8: Custom Sorting
// Sort by computed key (single-parameter closure)
def words = ["banana", "apple", "cherry", "date", "elderberry"]
def byLength = words.sort(false) { it.length() }
println "By length: ${byLength}"
// Sort with comparator (two-parameter closure)
def byLengthDesc = words.sort(false) { a, b -> b.length() <=> a.length() }
println "By length desc: ${byLengthDesc}"
// Sort objects by property
def people = [
[name: "Carol", age: 35],
[name: "Alice", age: 28],
[name: "Bob", age: 42],
[name: "Dave", age: 28]
]
// Sort by age
def byAge = people.sort(false) { it.age }
println "\nBy age: ${byAge.collect { "${it.name}(${it.age})" }}"
// Sort by age, then by name (multi-key sort)
def byAgeThenName = people.sort(false) { a, b ->
a.age <=> b.age ?: a.name <=> b.name
}
println "By age then name: ${byAgeThenName.collect { "${it.name}(${it.age})" }}"
// sort(false) returns a new list; sort() modifies in place
def original = [3, 1, 4, 1, 5, 9]
def sorted = original.sort(false)
println "\nOriginal after sort(false): ${original}"
println "Sorted copy: ${sorted}"
// The spaceship operator <=> makes comparison easy
println "\n1 <=> 2 = ${1 <=> 2}" // -1
println "2 <=> 2 = ${2 <=> 2}" // 0
println "3 <=> 2 = ${3 <=> 2}" // 1
Output
By length: [date, apple, banana, cherry, elderberry] By length desc: [elderberry, banana, cherry, apple, date] By age: [Alice(28), Dave(28), Carol(35), Bob(42)] By age then name: [Alice(28), Dave(28), Carol(35), Bob(42)] Original after sort(false): [3, 1, 4, 1, 5, 9] Sorted copy: [1, 1, 3, 4, 5, 9] 1 <=> 2 = -1 2 <=> 2 = 0 3 <=> 2 = 1
The sort(false) call is important: passing false as the first argument means “do not mutate the original list, return a new sorted copy.” Without it, sort() modifies the list in place. The spaceship operator <=> returns -1, 0, or 1 and makes comparator closures very clean.
Example 9: Chaining Higher-Order Functions for Data Processing
The real power of higher-order functions shows up when you chain them together to build data processing pipelines. Each method in the chain takes the output of the previous one and transforms it further.
Example 9: Chaining Functions
// Sales data processing pipeline
def sales = [
[product: "Widget", region: "North", amount: 150.00, quarter: "Q1"],
[product: "Gadget", region: "South", amount: 250.00, quarter: "Q1"],
[product: "Widget", region: "South", amount: 100.00, quarter: "Q1"],
[product: "Gadget", region: "North", amount: 300.00, quarter: "Q2"],
[product: "Widget", region: "North", amount: 200.00, quarter: "Q2"],
[product: "Doohickey", region: "South", amount: 50.00, quarter: "Q1"],
[product: "Gadget", region: "South", amount: 175.00, quarter: "Q2"],
[product: "Widget", region: "South", amount: 125.00, quarter: "Q2"],
]
// Pipeline 1: Top products by total revenue
def topProducts = sales
.groupBy { it.product }
.collectEntries { product, entries ->
[(product): entries.sum { it.amount }]
}
.sort { -it.value }
println "Revenue by product:"
topProducts.each { product, revenue ->
println " ${product}: \$${String.format('%.2f', revenue)}"
}
// Pipeline 2: Regional summary with averages
def regionSummary = sales
.groupBy { it.region }
.collectEntries { region, entries ->
def total = entries.sum { it.amount }
def avg = total / entries.size()
[(region): [total: total, avg: avg, count: entries.size()]]
}
println "\nRegion summary:"
regionSummary.each { region, stats ->
println " ${region}: ${stats.count} sales, total \$${String.format('%.2f', stats.total)}, avg \$${String.format('%.2f', stats.avg)}"
}
// Pipeline 3: Find the best quarter per product
def bestQuarters = sales
.groupBy { it.product }
.collectEntries { product, entries ->
def quarterTotals = entries
.groupBy { it.quarter }
.collectEntries { q, qEntries -> [(q): qEntries.sum { it.amount }] }
def bestQ = quarterTotals.max { it.value }
[(product): "${bestQ.key} (\$${String.format('%.2f', bestQ.value)})"]
}
println "\nBest quarter per product:"
bestQuarters.each { product, best -> println " ${product}: ${best}" }
Output
Revenue by product: Gadget: $725.00 Widget: $575.00 Doohickey: $50.00 Region summary: North: 3 sales, total $650.00, avg $216.67 South: 5 sales, total $700.00, avg $140.00 Best quarter per product: Widget: Q2 ($325.00) Gadget: Q2 ($475.00) Doohickey: Q1 ($50.00)
This is where Groovy really shines compared to Java. What would require verbose Stream API code with collectors and downstream grouping in Java becomes a clean, readable chain of method calls in Groovy. Each step in the pipeline is a simple higher-order function call.
Example 10: sum, min, max with Closure Transforms
Several Groovy collection methods accept an optional closure that transforms each element before performing the aggregation. This means you do not need a separate collect step before aggregating.
Example 10: sum, min, max with Closures
// sum with a closure transform
def orders = [
[item: "Book", price: 15.99, qty: 2],
[item: "Pen", price: 2.50, qty: 10],
[item: "Notebook", price: 8.00, qty: 3],
]
def totalRevenue = orders.sum { it.price * it.qty }
println "Total revenue: \$${String.format('%.2f', totalRevenue)}"
// sum of string lengths
def words = ["Groovy", "is", "awesome"]
def totalLength = words.sum { it.length() }
println "Total characters: ${totalLength}"
// min and max with closures
def people = [
[name: "Alice", age: 30],
[name: "Bob", age: 25],
[name: "Carol", age: 45],
[name: "Dave", age: 35]
]
def youngest = people.min { it.age }
def oldest = people.max { it.age }
println "\nYoungest: ${youngest.name} (${youngest.age})"
println "Oldest: ${oldest.name} (${oldest.age})"
// min/max by string length
def shortestWord = words.min { it.length() }
def longestWord = words.max { it.length() }
println "\nShortest word: ${shortestWord}"
println "Longest word: ${longestWord}"
// Combining sum with groupBy for subtotals
def salesByCategory = orders.groupBy {
it.price > 10 ? "Premium" : "Budget"
}.collectEntries { category, items ->
[(category): items.sum { it.price * it.qty }]
}
println "\nSales by category: ${salesByCategory}"
// count with a closure (Groovy's count method)
def mixedList = [1, "hello", 2, "world", 3, null, 4]
def intCount = mixedList.count { it instanceof Integer }
println "Integer count: ${intCount}"
Output
Total revenue: $80.98 Total characters: 15 Youngest: Bob (25) Oldest: Carol (45) Shortest word: is Longest word: awesome Sales by category: [Premium:31.98, Budget:49.00] Integer count: 4
Using closures directly in sum, min, and max is both more readable and more efficient than doing a collect followed by a separate aggregation, because it avoids creating an intermediate list.
collect vs collectEntries vs collectMany
Since these three methods have similar names but different behaviors, here is a direct comparison to help you choose the right one:
collect Variants Comparison
def items = ["hello", "world", "groovy"]
// collect: one-to-one transformation -> List
def upper = items.collect { it.toUpperCase() }
println "collect: ${upper}"
// [HELLO, WORLD, GROOVY]
// collectEntries: list to Map transformation -> Map
def lengthMap = items.collectEntries { [(it): it.length()] }
println "collectEntries: ${lengthMap}"
// [hello:5, world:5, groovy:6]
// collectMany: one-to-many transformation -> flat List
def chars = items.collectMany { it.toList() }
println "collectMany: ${chars}"
// [h, e, l, l, o, w, o, r, l, d, g, r, o, o, v, y]
Output
collect: [HELLO, WORLD, GROOVY] collectEntries: [hello:5, world:5, groovy:6] collectMany: [h, e, l, l, o, w, o, r, l, d, g, r, o, o, v, y]
In short: use collect for one-to-one transformations, collectEntries when you need a Map result, and collectMany when each element produces multiple output values that should be flattened.
Real-World Patterns
Here are the most common patterns you will encounter when using higher-order functions in production Groovy code:
Pattern 1: Transform and Filter
Transform and Filter Pattern
// Common pattern: findAll + collect
def users = [
[name: "Alice", active: true, email: "alice@test.com"],
[name: "Bob", active: false, email: "bob@test.com"],
[name: "Carol", active: true, email: "carol@test.com"]
]
def activeEmails = users
.findAll { it.active }
.collect { it.email }
println "Active emails: ${activeEmails}"
Output
Active emails: [alice@test.com, carol@test.com]
Pattern 2: Group and Aggregate
Group and Aggregate Pattern
// groupBy + collectEntries for pivot-style aggregation
def transactions = [
[type: "credit", amount: 100],
[type: "debit", amount: 50],
[type: "credit", amount: 200],
[type: "debit", amount: 75],
]
def summary = transactions
.groupBy { it.type }
.collectEntries { type, txns ->
[(type): [count: txns.size(), total: txns.sum { it.amount }]]
}
println "Summary: ${summary}"
Output
Summary: [credit:[count:2, total:300], debit:[count:2, total:125]]
Performance Considerations
Higher-order functions are expressive, but keep these performance tips in mind:
- Chaining creates intermediate collections — each
collect,findAll, orgroupByin a chain creates a new list or map. For very large collections, consider using Java Streams orinjectto do everything in a single pass. - Use inject for single-pass aggregation — when you need multiple aggregates (sum, count, min, max), a single
injectcall is more efficient than separate passes through the collection. - groupBy is eager — it processes the entire collection immediately. For lazy evaluation, consider using
stream().collect(Collectors.groupingBy(...)). - collect vs spread-dot —
names*.toUpperCase()is equivalent tonames.collect { it.toUpperCase() }for method calls. The spread-dot is shorter but collect is more flexible.
Conclusion
Higher-order functions are what make Groovy collections so delightful to work with. We covered the essential trio — groovy collect for transformation, inject for reduction, and groovy groupBy for partitioning — along with their useful cousins collectEntries, collectMany, countBy, sort, sum, min, and max.
The key insight is that these methods compose beautifully. Chain them together and you can express complex data processing logic in just a few readable lines. If you are coming from Java, this is where Groovy pays off the most.
Summary
collecttransforms each element one-to-one and returns a new listcollectEntriestransforms elements into key-value pairs and returns a MapcollectMany(flatMap) transforms one-to-many and flattens the resultinjectreduces a collection to a single accumulated value (fold/reduce)groupBypartitions elements into a Map of key to list of elementscountByis like groupBy but gives counts instead of element lists- Chain higher-order functions to build data processing pipelines
- Use
sort(false)to avoid mutating the original collection
If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.
Up next: Groovy Curry and Partial Application
Frequently Asked Questions
What is the difference between collect and collectEntries in Groovy?
collect transforms each element and returns a new List. collectEntries transforms each element into a key-value pair and returns a new Map. Use collect when you want a list of transformed values (e.g., names.collect { it.toUpperCase() } gives ['ALICE', 'BOB']). Use collectEntries when you need a Map (e.g., names.collectEntries { [(it): it.length()] } gives [Alice:5, Bob:3]).
How does Groovy groupBy work?
The groupBy method partitions a collection into a Map where each key is the result of applying the closure to an element, and each value is a list of all elements that produced that key. For example, [1,2,3,4,5].groupBy { it % 2 == 0 ? ‘even’ : ‘odd’ } returns [odd:[1,3,5], even:[2,4]]. You can also pass multiple closures for multi-level grouping.
What is inject in Groovy and how is it different from collect?
inject (also known as reduce or fold) processes a collection and accumulates a single result value. It takes an initial value and a two-parameter closure where the first parameter is the accumulator and the second is the current element. collect transforms each element independently and returns a list of the same size. inject reduces the entire collection down to one value, like a sum, concatenation, or complex aggregate.
What is collectMany in Groovy?
collectMany is Groovy’s version of flatMap. It applies a closure to each element where the closure returns a collection, then flattens all the returned collections into a single flat list. For example, [[1,2],[3,4],[5]].collectMany { it } returns [1,2,3,4,5]. It is useful when each input element maps to multiple output values, like splitting sentences into words: sentences.collectMany { it.split(' ') as List }.
Is Groovy collect the same as Java Stream map?
They are functionally similar — both transform each element of a collection using a function. However, Groovy collect is eager (it processes all elements immediately and returns a new list), while Java Stream map is lazy (it only processes elements when a terminal operation is called). For most use cases the result is the same, but for very large collections or infinite sequences, Java Streams offer lazy evaluation advantages. Groovy also supports Java Streams directly if needed.
Related Posts
Previous in Series: Groovy Closure Parameters – it, delegate, owner
Next in Series: Groovy Curry and Partial Application
Related Topics You Might Like:
Happy collecting, injecting, and grouping with Groovy!
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment