Lists & Arrays
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Groovy 6.0 (6.0.0, released 2026-09-22) and was run with JDK 26 on 2026-10-14. Lists
are the workhorse collection of Groovy: a literal in square brackets, a short operator vocabulary on top of
java.util.List, and a large set of GDK methods. Arrays are the plain Java arrays; Groovy gives them the same
subscript syntax and many of the same GDK methods. Maps are covered on Maps,
ranges (which are lists) on Ranges, and the iteration and transformation
methods (collect, findAll, inject, groupBy…) on
Working with Collections. The Java side of the story is on
Collections Framework and
Arrays.
List literals
A comma-separated list of expressions in square brackets creates a java.util.ArrayList. Lists are heterogeneous by
default and [] is the empty list. To get another implementation, coerce with as or declare the variable type: the
literal is then converted to that type.
def zones = ['north', 'south', 'east']
println zones.class.name
def queue = [1, 2, 3] as LinkedList
println queue.class.name
LinkedList other = [4, 5]
println other.class.name
def mixed = [1, 'two', 3.0, [4]]
println mixed*.class.simpleName
assert [] == []
assert ![] // an empty list is false in the Groovy Truth
assert zones.size() == 3
Output:
java.util.ArrayList
java.util.LinkedList
java.util.LinkedList
[Integer, String, BigDecimal, ArrayList]
The Groovy Truth treats an empty list as false (see Booleans & the Groovy Truth).
Indexing, negative indices and ranges
The subscript operator calls the GDK getAt and putAt methods. Indices are zero-based and may be negative to count from
the end (-1 is the last element). A range as index returns a new list (a copy, not a view), and a reversed range returns
the elements in reverse order. Several indices separated by commas pick several elements. Assigning to an index past the
end grows the list and fills the gap with null.
def readings = [18.5, 21.0, 19.25, 23.5, 20.0]
println readings[0]
println readings[-1]
println readings[1..3]
println readings[-2..-1]
println readings[3..1]
println readings[0, 2, 4]
readings[7] = 99.0
println readings
println readings.size()
def window = [1, 2, 3, 4, 5, 6]
window[1..2] = ['x'] // a range on the left replaces that slice
println window
Output:
18.5
20.0
[21.0, 19.25, 23.5]
[23.5, 20.0]
[23.5, 19.25, 21.0]
[18.5, 19.25, 20.0]
[18.5, 21.0, 19.25, 23.5, 20.0, null, null, 99.0]
8
[1, x, 4, 5, 6]
Reading an index past the end returns null rather than throwing Java’s IndexOutOfBoundsException, but a negative index
that reaches before the first element still throws (ArrayIndexOutOfBoundsException: Negative array index [-10] too large
for array size 3 in the Groovy 6.0.0 run used for this page). Ranges used as indices, including the exclusive forms, are
described on Ranges.
Operators on lists
Several operators are overloaded for lists through GDK methods (see Operator Overloading).
| Operator | GDK method | Effect |
|---|---|---|
|
|
appends one element in place and returns the same list, so it can be chained |
|
|
returns a new list with the elements of the right operand (or the single element) added |
|
|
returns a new list without the elements that occur in the right operand |
|
|
returns a new list repeated that many times |
|
|
spread-dot, calls a property or method on every element and collects the results |
def a = [1, 2]
a << 3 << 4 // mutates a, returns a
println a
println a + [5, 6] // new list
println a + 7 // new list
println a - [2, 3] // new list
println a * 2 // new list
println a // unchanged by the three lines above
println([[1, 2], [3, [4, 5]]].flatten())
def grid = [[1, 2, 3], [4, 5, 6]]
println grid[1][2]
println grid.transpose()
def names = [[name: 'north'], [name: 'south']]
println names*.name
Output:
[1, 2, 3, 4]
[1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 7]
[1, 4]
[1, 2, 3, 4, 1, 2, 3, 4]
[1, 2, 3, 4]
[1, 2, 3, 4, 5]
6
[[1, 4], [2, 5], [3, 6]]
[north, south]
Note that minus removes every occurrence of each element on the right, and + with a list argument concatenates
while << with a list argument appends the list as a single nested element. Nested lists are ordinary lists of lists;
flatten() returns a new, fully flattened list and transpose() swaps rows and columns of a rectangular list of lists.
Common list methods
The GDK adds dozens of methods to java.util.List. A few of the everyday ones, with the stack-like push and pop
working at the front of the list:
def l = [3, 1, 2]
println l.sort(false) // sorted copy, original untouched
println l
l.sort() // in place
println l
l.push(7) // inserts at index 0
println l
println l.pop() // removes index 0
println l
println l.head()
println l.tail()
println l.last()
println l.take(2)
println l.drop(1)
println l.collate(2)
println l.indexed()
println l.join('-')
println l.reverse() // copy
println l.reverse(true) // in place
println l
println l.toSet().class.simpleName
Output:
[1, 2, 3]
[3, 1, 2]
[1, 2, 3]
[7, 1, 2, 3]
7
[1, 2, 3]
1
[2, 3]
3
[1, 2]
[2, 3]
[[1, 2], [3]]
[0:1, 1:2, 2:3]
1-2-3
[3, 2, 1]
[3, 2, 1]
[3, 2, 1]
HashSet
Most transformation methods return a new list and leave the receiver alone. The exceptions that mutate carry either a
boolean mutate parameter (sort, reverse, unique) or are the java.util methods themselves (add, remove,
clear, removeAll). The full catalogue is in the GDK API for java.util.List, java.util.Collection and Iterable:
GDK: java.util.List.
Arrays
Groovy arrays are Java arrays: a fixed-length, covariant, typed block of memory. There is no array literal, so a list
literal is coerced: either by declaring the variable with an array type, or with as. Primitive arrays work the same way.
Groovy 3.0 added the Java-style initializer new int[] {1, 2, 3}, and Groovy 5.0 extended it to multi-dimensional arrays
written with the Java syntax (new int[][] {{1, 2}, {3, 4}}), while the list notation keeps working. A bare
{...} is still a closure, so int[] a = {1, 2, 3} is not an array initializer.
String[] names = ['north', 'south', 'east']
println names.class.name
def arr = ['x', 'y'] as String[]
println arr.getClass().simpleName
int[] ints = [3, 1, 2]
println ints.length
println ints.size() // GDK
println ints[-1] // negative index works on arrays too
println ints.sum()
println ints.collect { it * 2 } // result is a List, not an array
println ints[0..1].class.name // a range on an array also gives a List
int[] init = new int[] {1, 2, 3} // Java-style initializer (3.0)
println init
int[][] matrix = [[1, 2], [3, 4]]
println matrix[1][0]
int[][] jmat = new int[2][3]
println jmat.length + 'x' + jmat[0].length
int[][] literal = new int[][] {{1, 2}, {3, 4}}
println literal.collect { it.toList() }
println(new String[3] as List) // reference arrays default to null
Output:
[Ljava.lang.String;
String[]
3
3
2
6
[6, 2, 4]
java.util.ArrayList
[1, 2, 3]
3
2x3
[[1, 2], [3, 4]]
[null, null, null]
The GDK adds many of the list methods to arrays (each, collect, find, sum, max, join, toList, contains,
reverse, indexOf…); the 5.0 release notes describe over 220 new or enhanced methods on object and primitive arrays.
Coverage still differs by element type, so check the GDK API for the receiver you have. In the 6.0.0 run used here an
Integer[] accepts sort() (in place) and toSorted(), whereas the no-argument sort() and toSorted() raise
MissingMethodException on an int[]. Arrays.sort always works, and toList().sort() gives a sorted list:
Integer[] boxed = [3, 1, 2]
println boxed.toSorted()
boxed.sort() // in place
println boxed
int[] ints = [3, 1, 2]
try {
ints.sort()
} catch (MissingMethodException e) {
println 'no sort() on int[]'
}
println ints.toList().sort()
Arrays.sort(ints)
println Arrays.toString(ints)
String[] names = ['north', 'south', 'east']
String[] copy = names.clone() // a shallow copy of the array
copy[0] = 'west'
println names.toList()
println copy.toList()
Output:
[1, 2, 3]
[1, 2, 3]
no sort() on int[]
[1, 2, 3]
[1, 2, 3]
[north, south, east]
[west, south, east]
Arrays are fixed in size: there is no << or add on an array. Convert with toList(), as List or as Set when you
need to grow it, and back with as int[] or as String[].
Immutability
asImmutable() returns an immutable copy of the list and asUnmodifiable() returns a read-only view of the original. Both
throw UnsupportedOperationException on mutation. The difference shows when the original changes afterwards: the
view follows it, the immutable copy does not. Java’s List.of(...) is immutable as well. Immutability is shallow: the
elements themselves can still be mutated if they are mutable objects.
def base = [1, 2, 3]
def im = base.asImmutable()
try {
im << 4
} catch (UnsupportedOperationException e) {
println "immutable: ${e.class.simpleName}"
}
def view = base.asUnmodifiable()
base << 4
println view // the view reflects the later change
println im // the immutable copy does not
Output:
immutable: UnsupportedOperationException
[1, 2, 3, 4]
[1, 2, 3]
The output shows the difference: view sees the 4 appended to base, im does not. When you need a read-only snapshot
use asImmutable(); when you need to hand out a read-only window on a list you keep changing, use asUnmodifiable().
Copy versus modify
Assignment copies the reference, never the list. Mutating through any name changes what all the names see. The common ways
to copy are collect() with no closure, new ArrayList(list), list.clone() and list + []. All of them are shallow:
the nested lists are shared, so a deep copy has to copy the nested lists too.
def base = [1, 2, 3]
def same = base
same << 9
println base // alias: sees the change
def copy = base.collect()
copy << 5
println base // the copy is independent
println copy
println(new ArrayList(base).is(base))
println base.clone().is(base)
def nested = [[1], [2]]
def shallow = nested.collect()
shallow[0] << 100
println nested // nested lists are shared
def deep = nested.collect { it.collect() }
deep[0] << 200
println nested
println deep
Output:
[1, 2, 3, 9]
[1, 2, 3, 9]
[1, 2, 3, 9, 5]
false
false
[[1, 100], [2]]
[[1, 100], [2]]
[[1, 100, 200], [2]]
Concurrent modification
Removing elements from a collection while an iterator is running fails fast with ConcurrentModificationException, as in
Java. each is an iterator loop, so removing inside it fails. Collect what to keep with findAll, use the in-place
removeAll(Closure), or iterate over a copy.
def nums = [1, 2, 3, 4, 5, 6]
try {
nums.each { if (it % 2 == 0) nums.remove((Object) it) }
} catch (ConcurrentModificationException e) {
println 'ConcurrentModificationException'
}
def a = [1, 2, 3, 4, 5, 6]
a.removeAll { it % 2 == 0 } // in place, safe
println a
def b = [1, 2, 3, 4, 5, 6]
println b.findAll { it % 2 != 0 } // new list
def c = [1, 2, 3, 4]
for (n in c.collect()) { // iterate over a copy
if (n % 2 == 0) c.remove((Object) n)
}
println c
Output:
ConcurrentModificationException
[1, 3, 5]
[1, 3, 5]
[1, 3]
The (Object) it cast matters for a list of integers: remove(int) removes by index, remove(Object) by value.
Where the book is outdated
Older Groovy books describe lists and arrays as they were in the 1.x and 2.x lines and predate two changes: the
Java-style new int[] {...} initializer (3.0) and the Java multi-dimensional array syntax (5.0). They also predate the
large expansion of GDK methods for object and primitive arrays in 5.0, so advice to "convert to a list first" is often no
longer needed. They show def everywhere, whereas Groovy 3 and later also accept var. The list literal, the indexing rules
and the <<, `, `-` and `*+ operators described on this page are unchanged and remain the same in Groovy 6.0.