Maps

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. A map literal is a comma-separated list of key: value pairs in square brackets, and [:] is the empty map. The result is a java.util.LinkedHashMap, so iteration order is insertion order. Groovy adds property-style access, a large set of GDK methods and a few operators. Lists are covered on Lists & Arrays and the general iteration methods on Working with Collections; the Java side is on Collections Framework.

Map literals and access

Keys written as bare names are strings, not variable references. Numbers, booleans and quoted strings can be keys too. Values are read with the subscript m['k'], with property syntax m.k, or with get. Assigning with the same syntaxes adds or replaces an entry. Reading a missing key returns null; the two-argument get(key, default) is a GDK method that also stores the default.

def thresholds = [temperature: 28, humidity: 70]
println thresholds.getClass().name
println thresholds.temperature
println thresholds['humidity']
println thresholds.get('missing')

println thresholds.get('missing', 0)   // returns 0 and stores it
println thresholds

thresholds.light = 400
thresholds['co2'] = 800
println thresholds

println([:].isEmpty())
assert ![:]                            // an empty map is false in the Groovy Truth

Output:

java.util.LinkedHashMap
28
70
null
0
[temperature:28, humidity:70, missing:0]
[temperature:28, humidity:70, missing:0, light:400, co2:800]
true

Variable keys

Because a bare key is a string, a variable named key is not used in [key: 1]. Wrap the expression in parentheses to use its value:

def key = 'zone'
def wrong = [key: 1]
def right = [(key): 1]
println wrong
println right
println wrong.containsKey('key')
println right.containsKey('zone')

def mixed = [1: 'one', (2): 'two', 1.5: 'x']
println mixed[1]
println mixed.keySet().collect { it.class.simpleName }

Output:

[key:1]
[zone:1]
true
true
one
[Integer, Integer, BigDecimal]

Numeric keys are literal values, so 1: 'one' needs no parentheses. A quoted key containing spaces uses quotes in the literal (['my key': 1]) and either m.'my key' or m['my key'] to read it.

GString keys

A GString used as a key is a GStringImpl, not a String, and its hashCode differs from the equal-looking string’s, so a lookup with the plain string finds nothing. Convert with toString() when you build keys from interpolation (see Strings & GStrings).

def gk = ["a${1}": 1]
println gk.keySet()[0].class.simpleName
println gk['a1']                         // null: not found
println gk.get('a1')

def sk = [("a${1}".toString()): 1]
println sk['a1']

Output:

GStringImpl
null
null
1

Property-style access and the class pitfall

m.name is shorthand for m.get('name')/m.put('name', v) on a map, so it takes precedence over a real property of the map object. The most surprising case is m.class: on a map it is a key lookup, not the object’s class. Use m.getClass() for the class and m.'class' or m['class'] when you mean the key.

def m = [a: 1, b: 2]
println m.class                 // key lookup: no such key
m.class = 'oops'                // stores an entry named class
println m.class
println m.getClass()
println m

Output:

null
oops
class java.util.LinkedHashMap
[a:1, b:2, class:oops]

Choosing the implementation

The literal is a LinkedHashMap. Coerce with as or declare the variable type for other implementations; any Map class with a no-argument constructor can be targeted.

def tree = [b: 2, a: 1] as TreeMap
println tree.getClass().simpleName
println tree

TreeMap sorted = [c: 3, a: 1]
println sorted

Output:

TreeMap
[a:1, b:2]
[a:1, c:3]

Iterating and transforming

each, collect, findAll, find, any, every, count, inject and the other iteration methods accept a closure with either a single Map.Entry parameter or the pair key, value. collectEntries turns the entries into a new map, and groupBy partitions them by a key function. The sorting methods and subMap return new maps; findAll on a map returns a map, whereas collect returns a list.

def stock = [tomato: 12, basil: 5, mint: 0, lettuce: 7]

stock.each { k, v -> println "$k=$v" }
stock.each { e -> println e.key + ':' + e.value }
stock.eachWithIndex { e, i -> println "$i ${e.key}" }

println stock.collect { k, v -> "$k:$v" }
println stock.collectEntries { k, v -> [k.toUpperCase(), v * 2] }
println stock.findAll { k, v -> v > 5 }
println stock.find { k, v -> v == 0 }
println stock.count { k, v -> v > 0 }
println stock.groupBy { k, v -> v > 5 ? 'high' : 'low' }
println stock.subMap(['tomato', 'mint'])
println stock.sort { a, b -> a.value <=> b.value }
println stock.max { it.value }
println stock.values().sum()
println stock.keySet()
println stock.inject(0) { acc, k, v -> acc + v }
println stock.findResults { k, v -> v > 6 ? k : null }
println stock.findAll { it.value > 6 }.keySet()

Output:

tomato=12
basil=5
mint=0
lettuce=7
tomato:12
basil:5
mint:0
lettuce:7
0 tomato
1 basil
2 mint
3 lettuce
[tomato:12, basil:5, mint:0, lettuce:7]
[TOMATO:24, BASIL:10, MINT:0, LETTUCE:14]
[tomato:12, lettuce:7]
mint=0
3
[high:[tomato:12, lettuce:7], low:[basil:5, mint:0]]
[tomato:12, mint:0]
[mint:0, basil:5, lettuce:7, tomato:12]
tomato=12
24
[tomato, basil, mint, lettuce]
24
[tomato, lettuce]
[tomato, lettuce]

collectEntries is also the idiom for building a map from a list, and groupBy, countBy and indexed start from a list:

def words = ['ant', 'bee', 'cat', 'ape']
println words.groupBy { it[0] }
println words.countBy { it[0] }
println words.collectEntries { [(it): it.size()] }
println words.indexed()

Output:

[a:[ant, ape], b:[bee], c:[cat]]
[a:2, b:1, c:1]
[ant:3, bee:3, cat:3, ape:3]
[0:ant, 1:bee, 2:cat, 3:ape]

withDefault

withDefault wraps a map so that reading a missing key calls a closure, stores the result and returns it. It makes counting and grouping loops terse. Like get(key, default), it stores the default it computes, so reading an absent key adds it to the map.

def counts = [:].withDefault { 0 }
'mississippi'.each { counts[it]++ }
println counts

def groups = [:].withDefault { [] }
groups.a << 1
groups.a << 2
groups.b << 3
println groups
println groups.size()

def lookup = [:].withDefault { 'n/a' }
println lookup.zone                 // a read of an absent key
println lookup                      // ... has stored it

Output:

[m:1, i:4, s:4, p:2]
[a:[1, 2], b:[3]]
2
n/a
[zone:n/a]

Adding, removing and combining

+ returns a new map in which the entries of the right operand win; - removes the keys that also occur on the right. putAll, remove and put mutate. take, drop and takeWhile return new maps.

def stock = [tomato: 12, basil: 5, mint: 0, lettuce: 7]
println stock.remove('mint')
println stock
println stock + [kale: 2]
println stock + [basil: 99]
println stock - [basil: 5]
println stock.take(1)
println stock.drop(1)
println stock.takeWhile { k, v -> v > 6 }
println stock.getOrDefault('nothing', -1)
println stock.computeIfAbsent('dill') { 3 }
println stock

Output:

0
[tomato:12, basil:5, lettuce:7]
[tomato:12, basil:5, lettuce:7, kale:2]
[tomato:12, basil:99, lettuce:7]
[tomato:12, lettuce:7]
[tomato:12]
[basil:5, lettuce:7]
[tomato:12]
-1
3
[tomato:12, basil:5, lettuce:7, dill:3]

The spread map operator

*: spreads one map into another map literal (or into a named-argument method call). Later entries override earlier ones, which gives a one-line way to merge defaults with overrides. The *. spread-dot over a list of maps reads one key from every map (a GPath-style shortcut).

def defaults = [mode: 'auto', fan: 'low']
def overrides = [fan: 'high', light: 'on']
println([*:defaults, *:overrides])
println([a: 1, *:[b: 2], c: 3])

def hold = [[a: 1], [a: 2]]
println hold*.a
println hold.a

Output:

[mode:auto, fan:high, light:on]
[a:1, b:2, c:3]
[1, 2]
[1, 2]

Coercion with as and asType

A map literal can be coerced to a class or an interface with as (the call is asType). For a class, the entries become constructor named arguments (and then property setters). For an interface or a single-abstract-method type, a map of closures keyed by method name builds a dynamic implementation. The same map-to-object conversion happens implicitly on assignment to a typed variable.

class Bed { String name; int area }

def bed = [name: 'bed1', area: 12] as Bed
println bed.name + ' ' + bed.area

Bed second = [name: 'b2', area: 3]
println second.area

def r = [run: { println 'hello' }] as Runnable
r.run()

println(new Bed(name: 'x', area: 1).properties.subMap(['name', 'area']))
println(([name: 'bed1'] as Properties).getClass().simpleName)

Output:

bed1 12
3
hello
[name:x, area:1]
Properties

Read-only maps

asUnmodifiable() returns a read-only view and asImmutable() a read-only copy, with the same trade-off as for lists (see Lists & Arrays). Both throw UnsupportedOperationException on put and remove.

def un = [1: 'a'].asUnmodifiable()
try {
    un[2] = 'b'
} catch (UnsupportedOperationException e) {
    println 'unmodifiable'
}

def im = [1: 'a'].asImmutable()
try {
    im.put(2, 'b')
} catch (UnsupportedOperationException e) {
    println 'immutable'
}

def ordered = [b: 1, a: 2]
println ordered.keySet()
println ordered.sort().keySet()
println ordered.sort { it.value }.keySet()

Output:

unmodifiable
immutable
[b, a]
[a, b]
[b, a]

Where the book is outdated

Older books show map iteration only with each and entries. Current Groovy also has the key, value closure form shown above, collectEntries, withDefault and subMap, which they may not cover or cover with older signatures. Groovy 4.0 and later keep the same map literal and property-access semantics, so the class pitfall and the need for (key) are unchanged since those books.