Groovy Higher-Order Functions – collect, inject, groupBy with 10 Examples

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.

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, and groupBy
  • 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.

MethodPurposeReturns
collectTransform each elementNew list of transformed values
collectEntriesTransform into key-value pairsNew map
collectManyTransform and flattenNew flat list
injectReduce to single value (fold)Accumulated result
groupByPartition into groupsMap of key to list
countByCount by criterionMap of key to count
sumSum with optional transformTotal value
sortSort with custom comparatorSorted 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, or groupBy in a chain creates a new list or map. For very large collections, consider using Java Streams or inject to do everything in a single pass.
  • Use inject for single-pass aggregation — when you need multiple aggregates (sum, count, min, max), a single inject call 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-dotnames*.toUpperCase() is equivalent to names.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

  • collect transforms each element one-to-one and returns a new list
  • collectEntries transforms elements into key-value pairs and returns a Map
  • collectMany (flatMap) transforms one-to-many and flattens the result
  • inject reduces a collection to a single accumulated value (fold/reduce)
  • groupBy partitions elements into a Map of key to list of elements
  • countBy is 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.

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

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *