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

<<

leftShift

appends one element in place and returns the same list, so it can be chained

+

plus

returns a new list with the elements of the right operand (or the single element) added

-

minus

returns a new list without the elements that occur in the right operand

*

multiply

returns a new list repeated that many times

*.

collect

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.