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.