This guide covers Groovy lists with 15 practical examples. Create, access, sort, filter, and transform lists. Complete tutorial tested on Groovy 5.x.
“Lists are the bread and butter of any programming language. Master lists, and you’ve mastered data manipulation.”
Joshua Bloch, Effective Java
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 20 minutes
If you’re working with Groovy, you’re going to work with lists. A lot. Whether it’s processing API responses, transforming database results, or just wrangling configuration data, Groovy lists are the workhorse collection type you’ll reach for every single day.
What makes Groovy lists special is not just their clean syntax – the [] literal alone saves so much typing compared to Java’s new ArrayList<>() – but the dozens of powerful methods Groovy adds on top. Methods like collect(), findAll(), inject(), and groupBy() turn multi-line Java loops into elegant one-liners. This Groovy list tutorial covers it all with tested, working examples.
This post covers how to create, access, modify, sort, filter, and transform lists like a pro. For related topics, check out our guides on list to string conversion, contains, remove, and deduplicate, array manipulation, and array length and size.
Table of Contents
What Are Groovy Lists?
A Groovy list is an ordered collection of objects. Under the hood, the default list implementation is java.util.ArrayList, which means you get all the performance characteristics of ArrayList – fast random access, dynamic resizing, and full compatibility with Java’s Collections framework.
According to the official Groovy GDK Collections documentation, Groovy enhances Java lists with over 80 additional methods through the GDK (Groovy Development Kit). These methods bring functional programming patterns like map, filter, and reduce right into your list operations.
Key Points:
- Lists are created with square brackets:
[1, 2, 3] - Default type is
java.util.ArrayList - Lists can hold mixed types:
[1, 'hello', true, null] - Negative indices count from the end:
list[-1]returns the last element - Groovy adds 80+ methods via the GDK – each, collect, findAll, inject, groupBy, and many more
- Lists support operator overloading:
+,-,<<,*.
Creating Lists in Groovy
| Method | Syntax | Type | Mutable |
|---|---|---|---|
| Literal | [1, 2, 3] | ArrayList | Yes |
| Empty list | [] | ArrayList | Yes |
| as keyword | [1, 2, 3] as LinkedList | LinkedList | Yes |
| Constructor | new ArrayList([1, 2, 3]) | ArrayList | Yes |
| Range | (1..10).toList() | ArrayList | Yes |
| Immutable | [1, 2, 3].asImmutable() | Unmodifiable | No |
Ways to Create Lists
// 1. Literal syntax (most common)
def fruits = ['apple', 'banana', 'cherry']
println "${fruits} → ${fruits.getClass().name}"
// 2. Empty list
def empty = []
println "${empty} → ${empty.getClass().name}"
// 3. Mixed types (Groovy allows this)
def mixed = [1, 'hello', true, 3.14, null]
println "${mixed} → ${mixed.getClass().name}"
// 4. Using 'as' to choose a different List implementation
def linked = [1, 2, 3] as LinkedList
println "${linked} → ${linked.getClass().name}"
// 5. From a range
def numbers = (1..5).toList()
println "${numbers} → ${numbers.getClass().name}"
// 6. Using constructor
def copied = new ArrayList(['a', 'b', 'c'])
println "${copied} → ${copied.getClass().name}"
Output
[apple, banana, cherry] → java.util.ArrayList [] → java.util.ArrayList [1, hello, true, 3.14, null] → java.util.ArrayList [1, 2, 3] → java.util.LinkedList [1, 2, 3, 4, 5] → java.util.ArrayList [a, b, c] → java.util.ArrayList
Notice how every list literal defaults to java.util.ArrayList. If you need a specific implementation like LinkedList, use the as keyword. This is cleaner than Java’s verbose new LinkedList<>(Arrays.asList(...)).
Syntax and Basic Usage
Accessing Elements
Accessing List Elements
def langs = ['Java', 'Groovy', 'Kotlin', 'Scala', 'Clojure']
// Positive index (0-based)
println langs[0] // First element
println langs[2] // Third element
// Negative index (count from end)
println langs[-1] // Last element
println langs[-2] // Second to last
// Sublist with range
println langs[1..3] // Elements 1, 2, 3
println langs[0..<3] // Elements 0, 1, 2 (exclusive end)
// head, tail, first, last
println "First: ${langs.first()}"
println "Last: ${langs.last()}"
println "Head: ${langs.head()}"
println "Tail: ${langs.tail()}"
Output
Java Kotlin Clojure Scala [Groovy, Kotlin, Scala] [Java, Groovy, Kotlin] First: Java Last: Clojure Head: Java Tail: [Groovy, Kotlin, Scala, Clojure]
Negative indexing is one of those features that spoils you. Once you get used to list[-1] for the last element, Java’s list.get(list.size() - 1) feels painful. Also note: head() returns the first element, while tail() returns everything except the first.
15 Practical List Examples
Example 1: Adding Elements (add, <<, +)
What we’re doing: Adding elements to a list using three different approaches.
Example 1: Adding Elements
def list = [1, 2, 3]
// add() - mutates the list, returns boolean
list.add(4)
println "After add(4): ${list}"
// << operator (leftShift) - mutates, returns the list
list << 5
println "After << 5: ${list}"
// + operator - creates a NEW list (original unchanged)
def newList = list + 6
println "list + 6: ${newList}"
println "Original: ${list}"
// addAll - add multiple elements
list.addAll([7, 8, 9])
println "After addAll: ${list}"
// Insert at specific index
list.add(0, 0)
println "After add(0,0): ${list}"
Output
After add(4): [1, 2, 3, 4] After << 5: [1, 2, 3, 4, 5] list + 6: [1, 2, 3, 4, 5, 6] Original: [1, 2, 3, 4, 5] After addAll: [1, 2, 3, 4, 5, 7, 8, 9] After add(0,0): [0, 1, 2, 3, 4, 5, 7, 8, 9]
What happened here: The << operator is the most idiomatic Groovy way to append. The + operator is non-mutating – it returns a new list. This matters when you’re working with shared references or immutable data patterns.
Example 2: Removing Elements
What we’re doing: Removing elements by value, index, and condition.
Example 2: Removing Elements
def list = ['a', 'b', 'c', 'd', 'e', 'f']
// remove by index
list.remove(0)
println "After remove(0): ${list}"
// remove by value (note: for integers, use removeElement)
list.remove('c')
println "After remove('c'): ${list}"
// removeAt - explicit index removal
list.removeAt(0)
println "After removeAt(0): ${list}"
// minus operator - creates new list
def newList = list - 'e'
println "list - 'e': ${newList}"
println "Original: ${list}"
// removeAll with closure
def nums = [1, 2, 3, 4, 5, 6, 7, 8]
nums.removeAll { it % 2 == 0 }
println "Odds only: ${nums}"
Output
After remove(0): [b, c, d, e, f]
After remove('c'): [b, d, e, f]
After removeAt(0): [d, e, f]
list - 'e': [d, f]
Original: [d, e, f]
Odds only: [1, 3, 5, 7]
What happened here: Watch the remove() gotcha – for integer lists, remove(0) removes by index, not the value 0. Use removeElement(0) to remove the value. The - operator, like +, is non-mutating. For more on removing and deduplication, see Groovy List Contains, Remove, Deduplicate.
Example 3: each and eachWithIndex
What we’re doing: Iterating over lists using Groovy’s each methods.
Example 3: Iterating with each
def languages = ['Groovy', 'Java', 'Kotlin', 'Scala']
// each - simple iteration
print "Languages: "
languages.each { lang -> print "${lang} " }
println()
// eachWithIndex - iteration with index
languages.eachWithIndex { lang, idx ->
println " ${idx}: ${lang}"
}
// Implicit 'it' parameter
println "Uppercase:"
languages.each { println " ${it.toUpperCase()}" }
Output
Languages: Groovy Java Kotlin Scala 0: Groovy 1: Java 2: Kotlin 3: Scala Uppercase: GROOVY JAVA KOTLIN SCALA
What happened here: each() is the Groovy replacement for Java’s for-each loop. With closures, it’s more concise. Remember that each() always returns the original list, not the transformed values – use collect() for that.
Example 4: collect (Map/Transform)
What we’re doing: Transforming every element in a list to produce a new list.
Example 4: Transforming with collect
def numbers = [1, 2, 3, 4, 5]
// Square each number
def squares = numbers.collect { it * it }
println "Squares: ${squares}"
// Transform strings
def names = ['alice', 'bob', 'charlie']
def capitalized = names.collect { it.capitalize() }
println "Capitalized: ${capitalized}"
// Extract properties from objects
def people = [
[name: 'Alice', age: 30],
[name: 'Bob', age: 25],
[name: 'Charlie', age: 35]
]
def justNames = people.collect { it.name }
println "Names: ${justNames}"
// Chain transformations
def result = (1..10)
.collect { it * 2 }
.collect { "Item-${it}" }
println "Chained: ${result}"
Output
Squares: [1, 4, 9, 16, 25] Capitalized: [Alice, Bob, Charlie] Names: [Alice, Bob, Charlie] Chained: [Item-2, Item-4, Item-6, Item-8, Item-10, Item-12, Item-14, Item-16, Item-18, Item-20]
What happened here: collect() is Groovy’s equivalent of Java streams’ map(). It applies a closure to every element and returns a new list with the results. The original list stays untouched. For converting the result to a string, see Groovy List to String Conversion.
Example 5: findAll and find (Filter)
What we’re doing: Filtering elements that match a condition.
Example 5: Filtering with findAll and find
def numbers = [12, 5, 23, 8, 17, 3, 42, 31, 9]
// findAll - returns all matching elements
def evens = numbers.findAll { it % 2 == 0 }
println "Evens: ${evens}"
def overTen = numbers.findAll { it > 10 }
println "Over 10: ${overTen}"
// find - returns the FIRST matching element (or null)
def firstEven = numbers.find { it % 2 == 0 }
println "First even: ${firstEven}"
// findIndexOf - returns the index of first match
def idx = numbers.findIndexOf { it > 20 }
println "First >20 at index: ${idx} (value: ${numbers[idx]})"
// every - check if ALL match
println "All positive: ${numbers.every { it > 0 }}"
println "All even: ${numbers.every { it % 2 == 0 }}"
// any - check if ANY match
println "Any >40: ${numbers.any { it > 40 }}"
println "Any >50: ${numbers.any { it > 50 }}"
Output
Evens: [12, 8, 42] Over 10: [12, 23, 17, 42, 31] First even: 12 First >20 at index: 2 (value: 23) All positive: true All even: false Any >40: true Any >50: false
What happened here: findAll() is Groovy’s filter. It returns a new list of elements where the closure returns true. find() short-circuits and returns only the first match. Combine with every() and any() for boolean checks.
Example 6: inject (Reduce/Fold)
What we’re doing: Reducing a list to a single value using inject.
Example 6: Reducing with inject
def numbers = [1, 2, 3, 4, 5]
// Sum all elements
def sum = numbers.inject(0) { acc, val -> acc + val }
println "Sum: ${sum}"
// Product of all elements
def product = numbers.inject(1) { acc, val -> acc * val }
println "Product: ${product}"
// Build a string from a list
def words = ['Groovy', 'is', 'awesome']
def sentence = words.inject('') { acc, word ->
acc.isEmpty() ? word : "${acc} ${word}"
}
println "Sentence: ${sentence}"
// Find max manually (inject style)
def max = numbers.inject(Integer.MIN_VALUE) { acc, val -> val > acc ? val : acc }
println "Max: ${max}"
// Of course, Groovy has a simpler way
println "Max (simple): ${numbers.max()}"
println "Min (simple): ${numbers.min()}"
println "Sum (simple): ${numbers.sum()}"
Output
Sum: 15 Product: 120 Sentence: Groovy is awesome Max: 5 Max (simple): 5 Min (simple): 1 Sum (simple): 15
What happened here: inject() is Groovy’s reduce/fold operation. The first argument is the initial accumulator value, and the closure receives (accumulator, currentElement). While inject() is powerful, prefer sum(), max(), and min() when they fit.
Example 7: groupBy
What we’re doing: Grouping list elements by a key.
Example 7: Grouping with groupBy
// Group numbers by even/odd
def numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
def grouped = numbers.groupBy { it % 2 == 0 ? 'even' : 'odd' }
println "Grouped: ${grouped}"
// Group strings by first letter
def names = ['Alice', 'Bob', 'Anna', 'Brian', 'Charlie', 'Amy']
def byLetter = names.groupBy { it[0] }
println "By letter: ${byLetter}"
// Group objects by a property
def employees = [
[name: 'Alice', dept: 'Engineering'],
[name: 'Bob', dept: 'Marketing'],
[name: 'Charlie', dept: 'Engineering'],
[name: 'Diana', dept: 'Marketing'],
[name: 'Eve', dept: 'Engineering']
]
def byDept = employees.groupBy { it.dept }
byDept.each { dept, people ->
println "${dept}: ${people.collect { it.name }}"
}
Output
Grouped: [odd:[1, 3, 5, 7, 9], even:[2, 4, 6, 8, 10]] By letter: [A:[Alice, Anna, Amy], B:[Bob, Brian], C:[Charlie]] Engineering: [Alice, Charlie, Eve] Marketing: [Bob, Diana]
What happened here: groupBy() returns a Map where each key is the result of the closure and each value is a list of elements that produced that key. This is incredibly useful for categorizing data – the kind of thing that takes 10+ lines in Java.
Example 8: Sorting Lists
What we’re doing: Sorting lists with sort, sort with closures, and custom comparators.
Example 8: Sorting
// Basic sort (mutates the list)
def nums = [5, 3, 8, 1, 9, 2]
nums.sort()
println "Sorted: ${nums}"
// sort(false) - returns a NEW sorted list
def original = [5, 3, 8, 1, 9, 2]
def sorted = original.sort(false)
println "New sorted: ${sorted}"
println "Original: ${original}"
// Sort with closure (by string length)
def words = ['banana', 'fig', 'cherry', 'apple', 'kiwi']
words.sort { it.size() }
println "By length: ${words}"
// Sort with comparator closure (descending)
def desc = [5, 3, 8, 1, 9].sort { a, b -> b <=> a }
println "Descending: ${desc}"
// Sort objects by property
def people = [
[name: 'Charlie', age: 35],
[name: 'Alice', age: 28],
[name: 'Bob', age: 32]
]
people.sort { it.age }
println "By age: ${people.collect { "${it.name}(${it.age})" }}"
Output
Sorted: [1, 2, 3, 5, 8, 9] New sorted: [1, 2, 3, 5, 8, 9] Original: [5, 3, 8, 1, 9, 2] By length: [fig, kiwi, apple, banana, cherry] Descending: [9, 8, 5, 3, 1] By age: [Alice(28), Bob(32), Charlie(35)]
What happened here: By default, sort() mutates the list in place. Pass false as the first argument to get a new sorted list without touching the original. The spaceship operator <=> is great for custom comparators – swap a and b to reverse the order.
Example 9: flatten and collectMany
What we’re doing: Flattening nested lists into a single list.
Example 9: Flattening Lists
// flatten - fully flattens all levels
def nested = [[1, 2], [3, [4, 5]], [6]]
println "Nested: ${nested}"
println "Flattened: ${nested.flatten()}"
// collectMany - like flatMap
def sentences = ['Hello World', 'Groovy is great', 'Lists are fun']
def allWords = sentences.collectMany { it.split(' ').toList() }
println "All words: ${allWords}"
// Practical: get all skills from team members
def team = [
[name: 'Alice', skills: ['Java', 'Groovy']],
[name: 'Bob', skills: ['Python', 'Groovy', 'Go']],
[name: 'Charlie', skills: ['Java', 'Kotlin']]
]
def allSkills = team.collectMany { it.skills }.unique().sort()
println "Team skills: ${allSkills}"
Output
Nested: [[1, 2], [3, [4, 5]], [6]] Flattened: [1, 2, 3, 4, 5, 6] All words: [Hello, World, Groovy, is, great, Lists, are, fun] Team skills: [Go, Groovy, Java, Kotlin, Python]
What happened here: flatten() recursively flattens all levels of nesting. collectMany() is Groovy’s flatMap – it applies a closure that returns a list, then concatenates all the results. Chain with unique() and sort() for clean results.
Example 10: unique, intersect, disjoint
What we’re doing: Set-like operations on lists.
Example 10: Set Operations
// unique - remove duplicates (mutates list)
def dupes = [1, 3, 2, 3, 1, 4, 2, 5, 3]
dupes.unique()
println "Unique: ${dupes}"
// unique(false) - non-mutating
def original = [1, 3, 2, 3, 1, 4]
def uniq = original.unique(false)
println "Unique copy: ${uniq}"
println "Original: ${original}"
// unique with closure
def words = ['hello', 'HELLO', 'Hello', 'world', 'WORLD']
def caseInsensitive = words.unique(false) { it.toLowerCase() }
println "Case-insensitive unique: ${caseInsensitive}"
// intersect - common elements
def a = [1, 2, 3, 4, 5]
def b = [3, 4, 5, 6, 7]
println "Intersect: ${a.intersect(b)}"
// disjoint - check if lists share NO elements
println "Disjoint [1,2] and [3,4]: ${[1, 2].disjoint([3, 4])}"
println "Disjoint [1,2] and [2,3]: ${[1, 2].disjoint([2, 3])}"
Output
Unique: [1, 3, 2, 4, 5] Unique copy: [1, 3, 2, 4] Original: [1, 3, 2, 3, 1, 4] Case-insensitive unique: [hello, world] Intersect: [3, 4, 5] Disjoint [1,2] and [3,4]: true Disjoint [1,2] and [2,3]: false
What happened here: unique() removes duplicates preserving order. Like sort(), pass false to get a new list. intersect() finds common elements between two lists. disjoint() returns true if the lists share zero elements. For more details into deduplication, see Groovy List Contains, Remove, Deduplicate.
Example 11: Spread Operator (*.)
What we’re doing: Using the spread-dot operator to call a method on every element.
Example 11: Spread Operator
// Spread-dot calls a method on each element
def names = ['alice', 'bob', 'charlie']
println "Upper: ${names*.toUpperCase()}"
println "Sizes: ${names*.size()}"
// Equivalent to collect
assert names*.toUpperCase() == names.collect { it.toUpperCase() }
// Spread-dot with properties on objects
def people = [
[name: 'Alice', age: 30],
[name: 'Bob', age: 25],
[name: 'Charlie', age: 35]
]
println "Names: ${people*.name}"
println "Ages: ${people*.age}"
// Spread operator for method arguments
def addThree = { a, b, c -> a + b + c }
def args = [10, 20, 30]
println "Sum: ${addThree(*args)}"
Output
Upper: [ALICE, BOB, CHARLIE] Sizes: [5, 3, 7] Names: [Alice, Bob, Charlie] Ages: [30, 25, 35] Sum: 60
What happened here: The spread-dot operator *. is shorthand for collect. It calls a method or accesses a property on every element and returns the results as a list. It’s more concise than collect when you’re just calling a single method. The spread operator * (without the dot) unpacks a list into method arguments.
Example 12: subList and Ranges
What we’re doing: Extracting portions of a list.
Example 12: Sublists
def letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
// Range subscript (inclusive)
println "letters[2..5]: ${letters[2..5]}"
// Range subscript (exclusive end)
println "letters[2..<5]: ${letters[2..<5]}"
// Negative ranges
println "letters[-3..-1]: ${letters[-3..-1]}"
// Reverse range
println "letters[5..2]: ${letters[5..2]}"
// subList (Java method - exclusive end)
println "subList(1,4): ${letters.subList(1, 4)}"
// take and drop
println "take(3): ${letters.take(3)}"
println "drop(3): ${letters.drop(3)}"
println "take(-3): ${letters.takeRight(3)}"
println "drop(-3): ${letters.dropRight(3)}"
Output
letters[2..5]: [c, d, e, f] letters[2..<5]: [c, d, e] letters[-3..-1]: [f, g, h] letters[5..2]: [f, e, d, c] subList(1,4): [b, c, d] take(3): [a, b, c] drop(3): [d, e, f, g, h] take(-3): [f, g, h] drop(-3): [a, b, c, d, e]
What happened here: Groovy’s range subscript list[2..5] is inclusive on both ends – it returns elements at index 2, 3, 4, and 5. Use ..< for an exclusive end. Reverse ranges work too – list[5..2] returns elements in reverse. The take() and drop() methods are null-safe alternatives for similar operations. For more on arrays, see Groovy Array Manipulation.
Example 13: collect with Condition (List Comprehension)
What we’re doing: Simulating Python-style list comprehensions using collect and findAll.
Example 13: List Comprehensions
// Python: [x**2 for x in range(10) if x % 2 == 0]
// Groovy equivalent:
def evenSquares = (0..9).findAll { it % 2 == 0 }.collect { it ** 2 }
println "Even squares: ${evenSquares}"
// Cartesian product style
def suits = ['Hearts', 'Diamonds', 'Clubs', 'Spades']
def ranks = ['Ace', 'King', 'Queen']
def cards = suits.collectMany { suit ->
ranks.collect { rank -> "${rank} of ${suit}" }
}
println "Cards (first 6): ${cards.take(6)}"
println "Total cards: ${cards.size()}"
// FizzBuzz with collect
def fizzBuzz = (1..15).collect { n ->
if (n % 15 == 0) 'FizzBuzz'
else if (n % 3 == 0) 'Fizz'
else if (n % 5 == 0) 'Buzz'
else n
}
println "FizzBuzz: ${fizzBuzz}"
Output
Even squares: [0, 4, 16, 36, 64] Cards (first 6): [Ace of Hearts, King of Hearts, Queen of Hearts, Ace of Diamonds, King of Diamonds, Queen of Diamonds] Total cards: 12 FizzBuzz: [1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz]
What happened here: Groovy doesn’t have a built-in list comprehension syntax like Python, but findAll().collect() achieves the same result. collectMany() handles nested iteration elegantly – perfect for Cartesian products or flattening transformed results.
Example 14: countBy, count, and Aggregation
What we’re doing: Counting and aggregating list data.
Example 14: Counting and Aggregation
def words = ['apple', 'banana', 'avocado', 'blueberry', 'apricot', 'cherry', 'banana']
// count - count occurrences of a specific value
println "Bananas: ${words.count('banana')}"
// count with closure
println "Words starting with 'a': ${words.count { it.startsWith('a') }}"
// countBy - returns a frequency map
def byFirstLetter = words.countBy { it[0] }
println "Count by first letter: ${byFirstLetter}"
// Frequency map for numbers
def rolls = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
def frequency = rolls.countBy { it }
println "Frequency: ${frequency}"
// Aggregations
def scores = [85, 92, 78, 95, 88, 72, 91]
println "Count: ${scores.size()}"
println "Sum: ${scores.sum()}"
println "Average: ${scores.sum() / scores.size()}"
println "Max: ${scores.max()}"
println "Min: ${scores.min()}"
Output
Bananas: 2 Words starting with 'a': 3 Count by first letter: [a:3, b:3, c:1] Frequency: [3:2, 1:2, 4:1, 5:3, 9:1, 2:1, 6:1] Count: 7 Sum: 601 Average: 85.8571428571 Max: 95 Min: 72
What happened here: countBy() is one of Groovy’s hidden gems. It returns a map where each key is the closure result and each value is how many times that result appeared. Combined with sum(), max(), and min(), you have a complete analytics toolkit. For size-related operations, see Groovy Array Length and Size.
Example 15: Combinations – withIndex, collate, combinations, transpose
What we’re doing: Using advanced list methods for batching, combining, and restructuring.
Example 15: Advanced List Methods
// withIndex - pairs each element with its index
def colors = ['red', 'green', 'blue']
colors.withIndex().each { color, idx ->
println " ${idx}: ${color}"
}
// collate - split into batches
def items = (1..10).toList()
def batches = items.collate(3)
println "Batches of 3: ${batches}"
// collate with padding
def padded = items.collate(4, true)
println "Batches of 4: ${padded}"
// transpose - zip multiple lists
def keys = ['name', 'age', 'city']
def values = ['Alice', 30, 'NYC']
def pairs = [keys, values].transpose()
println "Transposed: ${pairs}"
println "As map: ${pairs.collectEntries { it }}"
// combinations - all possible combinations
def sizes = ['S', 'M']
def colorOpts = ['Red', 'Blue']
def combos = [sizes, colorOpts].combinations()
println "Combinations: ${combos}"
Output
0: red 1: green 2: blue Batches of 3: [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]] Batches of 4: [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10]] Transposed: [[name, Alice], [age, 30], [city, NYC]] As map: [name:Alice, age:30, city:NYC] Combinations: [[S, Red], [M, Red], [S, Blue], [M, Blue]]
What happened here: collate() splits a list into sub-lists of a given size – perfect for batch processing. transpose() works like Python’s zip(), pairing corresponding elements from multiple lists. combinations() generates the Cartesian product of multiple lists.
List Comprehensions with collect
While Groovy doesn’t have a dedicated list comprehension syntax like Python’s [x for x in range(10)], you can achieve the same thing – and often more readable – using collect() and findAll().
Groovy List Comprehensions
// Generate a list of formatted strings
def headers = (1..5).collect { "Column_${it}" }
println "Headers: ${headers}"
// Conditional generation
def matrix = (1..3).collect { row ->
(1..3).collect { col -> row * col }
}
println "Matrix:"
matrix.each { println " ${it}" }
// Filter and transform in one chain
def processed = ('A'..'Z').toList()
.findAll { it in ['A','E','I','O','U'] }
.collect { "${it}:${(int)it.charAt(0)}" }
println "Vowels with ASCII: ${processed}"
Output
Headers: [Column_1, Column_2, Column_3, Column_4, Column_5] Matrix: [1, 2, 3] [2, 4, 6] [3, 6, 9] Vowels with ASCII: [A:65, E:69, I:73, O:79, U:85]
The nested collect pattern generates a matrix – each outer iteration produces a row, and each inner collect builds the columns. This is a common pattern for generating tabular data or grid structures.
Immutable Lists
Sometimes you want a list that cannot be modified after creation. Groovy provides several ways to achieve this:
Immutable Lists
// asImmutable() - wraps list as unmodifiable
def mutable = [1, 2, 3]
def immutable = mutable.asImmutable()
println "Immutable list: ${immutable}"
println "Type: ${immutable.getClass().name}"
// Reading works fine
println "First: ${immutable[0]}"
println "Size: ${immutable.size()}"
// Writing throws UnsupportedOperationException
try {
immutable.add(4)
} catch (UnsupportedOperationException e) {
println "Cannot modify: ${e.getClass().simpleName}"
}
// Collections.unmodifiableList (Java way)
def javaImmutable = Collections.unmodifiableList([10, 20, 30])
println "Java immutable: ${javaImmutable}"
// asUnmodifiable() - same as asImmutable()
def also = [4, 5, 6].asUnmodifiable()
println "Unmodifiable: ${also}"
try {
also << 7
} catch (UnsupportedOperationException e) {
println "Cannot add to unmodifiable: ${e.getClass().simpleName}"
}
Output
Immutable list: [1, 2, 3] Type: java.util.Collections$UnmodifiableRandomAccessList First: 1 Size: 3 Cannot modify: UnsupportedOperationException Java immutable: [10, 20, 30] Unmodifiable: [4, 5, 6] Cannot add to unmodifiable: UnsupportedOperationException
Important:
asImmutable()creates a wrapper around the original list. If you still hold a reference to the original mutable list, modifying it will also change the “immutable” view. To be truly safe, don’t keep a reference to the original.
Edge Cases and Best Practices
Best Practices Summary
DO:
- Use
[]literal syntax for creating lists – it’s clean and idiomatic - Use
<<(leftShift) for appending – it’s the Groovy way - Use
sort(false)andunique(false)when you need a new list without mutating the original - Prefer
findAll+collectchains over manual loops - Use
*.spread-dot when calling a single method on all elements
DON’T:
- Confuse
remove(int)(by index) withremoveElement(Object)(by value) on integer lists - Modify a list while iterating over it with
each– useremoveAllorfindAllinstead - Forget that
sort()mutates the list by default – this catches many beginners - Use
eachwhen you need return values – usecollectinstead
Performance Considerations
Groovy lists are backed by ArrayList by default, so all the standard Java performance characteristics apply:
Performance Tips
// Tip 1: Pre-size when you know the count
def bigList = new ArrayList(10000)
10000.times { bigList << it }
println "Pre-sized list: ${bigList.size()} items"
// Tip 2: Use findAll instead of manual removal in loops
def data = (1..10000).toList()
// Fast: findAll creates a filtered copy
def evens = data.findAll { it % 2 == 0 }
println "Evens: ${evens.size()}"
// Tip 3: Use collectEntries for list-to-map conversion
def items = ['apple', 'banana', 'cherry']
def lengthMap = items.collectEntries { [it, it.size()] }
println "Lengths: ${lengthMap}"
// Tip 4: Use inject/sum for accumulation
def total = (1..100).sum()
println "Sum 1..100: ${total}"
Output
Pre-sized list: 10000 items Evens: 5000 Lengths: [apple:5, banana:6, cherry:6] Sum 1..100: 5050
For most applications, ArrayList is the right choice. If you need frequent insertions/removals in the middle of large lists, consider [1, 2, 3] as LinkedList. For thread-safe lists, use Collections.synchronizedList([]) or new CopyOnWriteArrayList().
Real-World Examples
Example: Processing CSV Data
Real-World: CSV Processing
// Simulate CSV lines
def csvData = [
'Name,Department,Salary',
'Alice,Engineering,95000',
'Bob,Marketing,72000',
'Charlie,Engineering,88000',
'Diana,Marketing,78000',
'Eve,Engineering,102000'
]
// Parse CSV into list of maps
def headers = csvData[0].split(',')
def records = csvData.drop(1).collect { line ->
def values = line.split(',')
[headers, values].transpose().collectEntries { it }
}
println "Records:"
records.each { println " ${it}" }
// Average salary by department
def avgByDept = records
.groupBy { it.Department }
.collectEntries { dept, people ->
def avg = people.collect { it.Salary.toInteger() }.sum() / people.size()
[dept, avg]
}
println "\nAvg salary by dept: ${avgByDept}"
// Find highest paid
def topEarner = records.max { it.Salary.toInteger() }
println "Top earner: ${topEarner.Name} (\$${topEarner.Salary})"
Output
Records: [Name:Alice, Department:Engineering, Salary:95000] [Name:Bob, Department:Marketing, Salary:72000] [Name:Charlie, Department:Engineering, Salary:88000] [Name:Diana, Department:Marketing, Salary:78000] [Name:Eve, Department:Engineering, Salary:102000] Avg salary by dept: [Engineering:95000, Marketing:75000] Top earner: Eve ($102000)
Example: Pipeline-Style Data Processing
Real-World: Data Pipeline
// Simulate log entries
def logs = [
[time: '10:01', level: 'INFO', msg: 'Server started'],
[time: '10:02', level: 'DEBUG', msg: 'Loading config'],
[time: '10:03', level: 'ERROR', msg: 'DB connection failed'],
[time: '10:04', level: 'WARN', msg: 'Retrying connection'],
[time: '10:05', level: 'INFO', msg: 'DB connected'],
[time: '10:06', level: 'ERROR', msg: 'Auth service timeout'],
[time: '10:07', level: 'INFO', msg: 'Auth service recovered']
]
// Pipeline: find errors, format them, and create a report
def errorReport = logs
.findAll { it.level == 'ERROR' }
.collect { "[${it.time}] ${it.msg}" }
.join('\n')
println "Error Report:"
println errorReport
// Count by log level
def summary = logs.countBy { it.level }
println "\nLog Summary: ${summary}"
// Check if any critical errors
def hasErrors = logs.any { it.level == 'ERROR' }
println "Has errors: ${hasErrors}"
Output
Error Report: [10:03] DB connection failed [10:06] Auth service timeout Log Summary: [INFO:3, DEBUG:1, ERROR:2, WARN:1] Has errors: true
This is where Groovy lists really shine. The pipeline style – findAll().collect().join() – reads almost like English. In Java, the equivalent code would require streams, collectors, and significantly more boilerplate.
Conclusion
This Groovy list tutorial covered everything from basic creation to advanced data processing. Lists in Groovy are fundamentally java.util.ArrayList objects, but the GDK transforms them into powerful data manipulation tools with methods like collect(), findAll(), inject(), groupBy(), and collectMany().
The most important things to remember: << for appending, collect() for transforming, findAll() for filtering, and sort(false) when you want a non-mutating sort. These four patterns will cover 90% of your list operations.
For deeper dives into specific list operations, check out the related posts below.
Summary
- Groovy lists default to
java.util.ArrayList– useas LinkedListfor other types - Negative indices count from the end:
list[-1]is the last element <<mutates,+creates a new list – know the differencecollect()= map,findAll()= filter,inject()= reduce- The spread-dot operator
*.is shorthand forcollect { it.method() }
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 List to String – All Conversion Methods
Frequently Asked Questions
What is the default type of a Groovy list?
When you create a list with the literal syntax [1, 2, 3], Groovy creates a java.util.ArrayList. You can verify this with [1, 2, 3].getClass().name. To use a different implementation like LinkedList, use the ‘as’ keyword: [1, 2, 3] as LinkedList.
How do I add an element to a Groovy list?
Use the << (leftShift) operator for the most idiomatic approach: list << element. You can also use add(), addAll(), or the + operator. Note that << and add() mutate the original list, while + creates a new list.
What is the difference between collect and each in Groovy?
each() iterates over elements and always returns the original list – it’s for side effects like printing. collect() transforms each element and returns a NEW list with the results. Use each when you don’t need return values, and collect when you need to transform data.
How do I access the last element of a Groovy list?
Use negative indexing: list[-1] returns the last element, list[-2] returns the second-to-last, and so on. You can also use list.last(). Both approaches are clean and concise compared to Java’s list.get(list.size() - 1).
Does Groovy sort() modify the original list?
Yes, sort() mutates the original list by default. To get a sorted copy without modifying the original, pass false as the first argument: def sorted = list.sort(false). The same applies to unique() – use unique(false) for a non-mutating version.
Related Posts
Previous in Series: Groovy Add to Map – All Methods Explained
Next in Series: Groovy List to String – All Conversion Methods
Related Topics You Might Like:
- Groovy List Contains, Remove, and Deduplicate
- Groovy Array Manipulation – Complete Guide
- Groovy Array Length and Size – All Methods
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment