15 Essential Groovy List Methods Every Developer Must Know

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.

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

MethodSyntaxTypeMutable
Literal[1, 2, 3]ArrayListYes
Empty list[]ArrayListYes
as keyword[1, 2, 3] as LinkedListLinkedListYes
Constructornew ArrayList([1, 2, 3])ArrayListYes
Range(1..10).toList()ArrayListYes
Immutable[1, 2, 3].asImmutable()UnmodifiableNo

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) and unique(false) when you need a new list without mutating the original
  • Prefer findAll + collect chains over manual loops
  • Use *. spread-dot when calling a single method on all elements

DON’T:

  • Confuse remove(int) (by index) with removeElement(Object) (by value) on integer lists
  • Modify a list while iterating over it with each – use removeAll or findAll instead
  • Forget that sort() mutates the list by default – this catches many beginners
  • Use each when you need return values – use collect instead

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 – use as LinkedList for other types
  • Negative indices count from the end: list[-1] is the last element
  • << mutates, + creates a new list – know the difference
  • collect() = map, findAll() = filter, inject() = reduce
  • The spread-dot operator *. is shorthand for collect { 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.

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:

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 *