Working with Groovy maps is simple once you see these 12 practical examples. Create, access, iterate, filter, and transform maps. Complete tutorial tested on Groovy 5.x.
“A map is the Swiss Army knife of data structures. Once you learn to wield it, you’ll reach for it instinctively in every project.”
Fred Brooks, The Mythical Man-Month
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 20 minutes
If you’ve been writing Groovy for more than a few days, you’ve already used a Groovy map without even realizing it. Every time you declare a named parameter in a method call, pass options to a builder, or parse JSON – maps are doing the heavy lifting behind the scenes.
Maps are one of those data structures that show up everywhere. Configuration files, API responses, caching layers, database result sets – they all boil down to key-value pairs. And Groovy makes working with maps ridiculously easy compared to Java. No verbose new HashMap<String, Object>(), no Map.of() limitations. Just clean, concise map literals.
This Groovy map tutorial walks you through everything – from creating your first map to advanced techniques like groupBy, spread operators, and nested map traversal. We’ll cover 12 practical examples, each tested on Groovy 5.x with actual output you can verify yourself. If you’re looking for specific operations like adding entries, check out our dedicated post on adding to maps in Groovy. For list operations and conversions, see our Groovy list tutorial and list to string conversion guide.
Table of Contents
What Is a Groovy Map?
A Groovy map is a collection of key-value pairs where each key is unique. Think of it like a dictionary: you look up a word (the key) to find its definition (the value). Under the hood, Groovy maps default to java.util.LinkedHashMap, which means they preserve insertion order – something that catches Java developers off guard since Java’s HashMap does not.
According to the official Groovy documentation on maps, Groovy enhances Java’s Map interface with dozens of additional methods through the GDK (Groovy Development Kit). These methods make operations like filtering, transforming, and iterating over maps feel natural and expressive.
Key Points:
- Maps store data as key-value pairs – each key maps to exactly one value
- The default implementation is
LinkedHashMap(preserves insertion order) - Keys are strings by default in map literals (no quotes needed)
- Values can be any type – strings, numbers, lists, other maps, closures
- An empty map is created with
[:](not[]– that’s an empty list) - Groovy adds 30+ extra methods to Java’s Map through the GDK
Why Use Maps in Groovy?
You might wonder – why not just use a class with fields? Or a list of pairs? Here’s why maps are the go-to choice in Groovy:
- Concise syntax:
[name: 'Alice', age: 30]– that’s it, no class definition needed - Dynamic keys: Unlike class fields, map keys can be computed at runtime
- JSON-compatible: Maps naturally represent JSON structures, making API work simple
- Named parameters: Groovy methods accept maps as the first argument for named parameters
- Configuration: Maps are perfect for config objects, settings, and feature flags
- O(1) lookup: Finding a value by key is constant time, unlike searching through a list
When to Use a Map
Reach for a map when you need to:
- Associate values with unique keys (user IDs to user objects, config keys to values)
- Count occurrences of items (word frequency, log event counts)
- Group related data without creating a formal class
- Parse or build JSON/XML structures
- Pass multiple named options to a method
- Cache computed results for fast retrieval
If your data is ordered and you access it by position, use a list instead. If your data has a fixed, well-known structure, consider a class or record. Maps shine when your keys are dynamic or your structure is flexible.
How Maps Work Internally
When you write def m = [name: 'Alice'], Groovy creates a java.util.LinkedHashMap. This is important for two reasons:
- Insertion order is preserved. When you iterate a Groovy map, entries come out in the order you put them in. Java’s HashMap doesn’t guarantee this.
- Keys are hashed. Lookups, inserts, and deletes are O(1) on average. The map uses
hashCode()andequals()on keys, so your key objects need to implement these correctly.
One thing that trips people up: in map literals, keys without quotes are treated as strings, not variable references. So [name: 'Alice'] means the key is the string "name", not the value of some variable called name. If you want a variable as a key, wrap it in parentheses: [(name): 'Alice'].
Groovy Map Syntax and Creation
Here are all the ways you can create a Groovy map:
Creating Groovy Maps
// 1. Map literal (most common)
def person = [name: 'Alice', age: 30, city: 'NYC']
println person
println "Type: ${person.getClass().name}"
// 2. Empty map
def empty = [:]
println "Empty map: ${empty}"
println "Empty type: ${empty.getClass().name}"
// 3. With explicit types
Map<String, Integer> scores = [math: 95, science: 88, english: 92]
println scores
// 4. Using Java constructor
def javaMap = new HashMap()
javaMap.put('key1', 'value1')
println javaMap
// 5. TreeMap for sorted keys
def sorted = new TreeMap([banana: 2, apple: 5, cherry: 1])
println "Sorted: ${sorted}"
// 6. Variable as key (parentheses required)
def keyName = 'language'
def config = [(keyName): 'Groovy', version: '5.0']
println config
Output
[name:Alice, age:30, city:NYC] Type: java.util.LinkedHashMap Empty map: [:] Empty type: java.util.LinkedHashMap [math:95, science:88, english:92] [key1:value1] Sorted: [apple:5, banana:2, cherry:1] [language:Groovy, version:5.0]
Notice how the default type is always LinkedHashMap. The keys in map literals are automatically treated as strings – you don’t need quotes around them. But if you want a variable’s value as the key, wrap it in parentheses like [(keyName): value].
Quick Reference: Common Groovy Map Operations
| Operation | Syntax | Example | Result |
|---|---|---|---|
| Create | [key: value] | [a: 1, b: 2] | Map with 2 entries |
| Access (dot) | map.key | map.a | 1 |
| Access (bracket) | map['key'] | map['a'] | 1 |
| Add/Update | map.key = val | map.c = 3 | Adds c:3 |
| Remove | map.remove(key) | map.remove('a') | Removes a |
| Size | map.size() | [a:1].size() | 1 |
| Contains key | map.containsKey(k) | map.containsKey('a') | true |
| Get with default | map.get(k, def) | map.get('z', 0) | 0 |
| Iterate | map.each { k, v -> } | map.each { println it } | Each entry |
| Filter | map.findAll { k, v -> } | map.findAll { k,v -> v > 1 } | Filtered map |
12 Practical Groovy Map Examples
Example 1: Accessing Map Values – Dot Notation vs Bracket Syntax
What we’re doing: Exploring all the ways to read values from a Groovy map.
Example 1: Accessing Map Values
def user = [name: 'Alice', age: 30, email: 'alice@example.com']
// 1. Dot notation (most common)
println "Name: ${user.name}"
// 2. Bracket notation (works with special chars)
println "Age: ${user['age']}"
// 3. get() method
println "Email: ${user.get('email')}"
// 4. get() with default value (doesn't modify the map)
println "Phone: ${user.get('phone', 'N/A')}"
println "Map still has no phone: ${user.containsKey('phone')}"
// 5. getOrDefault() - Java 8 method
println "City: ${user.getOrDefault('city', 'Unknown')}"
// 6. Null for missing keys
println "Missing key: ${user.address}"
println "Is it null? ${user.address == null}"
Output
Name: Alice Age: 30 Email: alice@example.com Phone: N/A Map still has no phone: false City: Unknown Missing key: null Is it null? true
What happened here: Groovy lets you access map values with dot notation (map.key) just like object properties. The bracket syntax map['key'] is useful when keys contain special characters or spaces. The get() method with a default value is handy for avoiding null checks – but note that it does not add the key to the map.
Example 2: Adding and Removing Map Entries
What we’re doing: Modifying a Groovy map by adding, updating, and removing entries.
Example 2: Add and Remove Entries
def cart = [apple: 3, banana: 5]
println "Initial: ${cart}"
// Add new entries
cart.cherry = 2
cart['mango'] = 4
cart.put('grape', 6)
println "After adding: ${cart}"
// Update existing entry
cart.apple = 10
println "After update: ${cart}"
// Remove by key
cart.remove('banana')
println "After remove: ${cart}"
// Remove by key-value pair (only removes if value matches)
def removed = cart.remove('cherry', 2)
println "Removed cherry=2? ${removed}"
println "Final: ${cart}"
// Clear all entries
cart.clear()
println "After clear: ${cart}"
Output
Initial: [apple:3, banana:5] After adding: [apple:3, banana:5, cherry:2, mango:4, grape:6] After update: [apple:10, banana:5, cherry:2, mango:4, grape:6] After remove: [apple:10, cherry:2, mango:4, grape:6] Removed cherry=2? true Final: [apple:10, mango:4, grape:6] After clear: [:]
What happened here: You can add entries using dot notation, bracket syntax, or put(). All three do the same thing. The two-argument remove(key, value) is great for conditional removal – it only removes the entry if the value matches. For a deeper look at all the ways to add entries, see Groovy Add to Map.
Example 3: Iterating Over a Groovy Map with each()
What we’re doing: Walking through every entry in a map using different iteration styles.
Example 3: Iterating Maps
def scores = [Alice: 95, Bob: 82, Charlie: 91, Diana: 88]
// Style 1: each with entry (single parameter)
println "=== Style 1: entry ==="
scores.each { entry ->
println "${entry.key}: ${entry.value}"
}
// Style 2: each with key, value (two parameters)
println "\n=== Style 2: key, value ==="
scores.each { name, score ->
println "${name} scored ${score}"
}
// Style 3: eachWithIndex
println "\n=== Style 3: eachWithIndex ==="
scores.eachWithIndex { entry, idx ->
println "${idx + 1}. ${entry.key} = ${entry.value}"
}
// Style 4: iterate just keys or just values
println "\nNames: ${scores.keySet().join(', ')}"
println "Scores: ${scores.values().join(', ')}"
Output
=== Style 1: entry === Alice: 95 Bob: 82 Charlie: 91 Diana: 88 === Style 2: key, value === Alice scored 95 Bob scored 82 Charlie scored 91 Diana scored 88 === Style 3: eachWithIndex === 1. Alice = 95 2. Bob = 82 3. Charlie = 91 4. Diana = 88 Names: Alice, Bob, Charlie, Diana Scores: 95, 82, 91, 88
What happened here: The two-parameter closure { key, value -> } is the cleanest way to iterate. With a single parameter, you get a Map.Entry object. The eachWithIndex variant gives you the position – useful for numbered output. Since maps are LinkedHashMap by default, the order is always predictable.
Example 4: Filtering Maps with findAll()
What we’re doing: Selecting entries that match a condition.
Example 4: Filtering with findAll
def products = [laptop: 999, phone: 699, tablet: 449, watch: 299, earbuds: 149]
// Find all products over $500
def premium = products.findAll { key, value -> value > 500 }
println "Premium: ${premium}"
// Find all products under $300
def budget = products.findAll { k, v -> v < 300 }
println "Budget: ${budget}"
// Find first match with find()
def first = products.find { k, v -> v > 400 }
println "First over \$400: ${first}"
// Check if any/every match
println "Any over \$900? ${products.any { k, v -> v > 900 }}"
println "All over \$100? ${products.every { k, v -> v > 100 }}"
// Count matches
def count = products.count { k, v -> v >= 300 && v <= 700 }
println "Products \$300-\$700: ${count}"
Output
Premium: [laptop:999, phone:699] Budget: [watch:299, earbuds:149] First over $400: laptop=999 Any over $900? true All over $100? true Products $300-$700: 2
What happened here: findAll() returns a new map with only the matching entries. find() returns the first matching Map.Entry. The any() and every() methods are perfect for quick boolean checks without building a filtered map.
Example 5: Transforming Maps with collect()
What we’re doing: Transforming map entries into a list or a new map.
Example 5: Transforming with collect
def prices = [coffee: 4.50, tea: 3.00, juice: 5.50, water: 1.50]
// collect() - transforms to a list
def labels = prices.collect { name, price ->
"${name.capitalize()}: \$${price}"
}
println "Labels: ${labels}"
// collectEntries() - transforms to a new map
def discounted = prices.collectEntries { name, price ->
[name, (price * 0.8).round(2)]
}
println "20% off: ${discounted}"
// Transform keys
def upperKeys = prices.collectEntries { k, v -> [k.toUpperCase(), v] }
println "Upper keys: ${upperKeys}"
// Swap keys and values
def swapped = prices.collectEntries { k, v -> [v, k] }
println "Swapped: ${swapped}"
Output
Labels: [Coffee: $4.50, Tea: $3.00, Juice: $5.50, Water: $1.50] 20% off: [coffee:3.60, tea:2.40, juice:4.40, water:1.20] Upper keys: [COFFEE:4.50, TEA:3.00, JUICE:5.50, WATER:1.50] Swapped: [4.50:coffee, 3.00:tea, 5.50:juice, 1.50:water]
What happened here: The big distinction is collect() returns a list, while collectEntries() returns a map. Use collectEntries when you want to reshape a map – change keys, transform values, or even swap keys and values. Just be careful when swapping: if two entries have the same value, you’ll lose one.
Example 6: Sorting Maps
What we’re doing: Sorting a Groovy map by keys and by values.
Example 6: Sorting Maps
def inventory = [banana: 45, apple: 120, cherry: 30, date: 85]
// Sort by key (alphabetical)
def byKey = inventory.sort()
println "By key: ${byKey}"
// Sort by value (ascending)
def byValue = inventory.sort { a, b -> a.value <=> b.value }
println "By value (asc): ${byValue}"
// Sort by value (descending)
def byValueDesc = inventory.sort { a, b -> b.value <=> a.value }
println "By value (desc): ${byValueDesc}"
// IMPORTANT: sort() modifies the original map!
// Use toSorted() for a new sorted copy
def original = [z: 1, a: 2, m: 3]
def sortedCopy = original.toSorted { a, b -> a.key <=> b.key }
println "Original unchanged: ${original}"
println "Sorted copy: ${sortedCopy}"
Output
By key: [apple:120, banana:45, cherry:30, date:85] By value (asc): [cherry:30, banana:45, date:85, apple:120] By value (desc): [apple:120, date:85, banana:45, cherry:30] Original unchanged: [z:1, a:2, m:3] Sorted copy: [a:2, m:3, z:1]
What happened here: By default, sort() modifies the map in place and sorts by key. The spaceship operator <=> makes comparisons clean. Pass false as the first argument to get a sorted copy without touching the original – this is often what you actually want.
Example 7: Merging Maps
What we’re doing: Combining two or more maps together.
Example 7: Merging Maps
def defaults = [theme: 'light', language: 'en', fontSize: 14]
def userPrefs = [theme: 'dark', fontSize: 16]
// Method 1: Plus operator (creates new map)
def merged = defaults + userPrefs
println "Merged: ${merged}"
// Method 2: putAll (modifies in place)
def config = [:]
config.putAll(defaults)
config.putAll(userPrefs)
println "Config: ${config}"
// Method 3: Left-shift operator (adds entries)
def map1 = [a: 1, b: 2]
map1 << [c: 3, d: 4]
println "After <<: ${map1}"
// Method 4: Spread operator for merging multiple maps
def base = [x: 1]
def extra1 = [y: 2]
def extra2 = [z: 3]
def combined = [*:base, *:extra1, *:extra2]
println "Spread merge: ${combined}"
// Conflict resolution: last value wins
def a = [name: 'Alice', role: 'user']
def b = [name: 'Bob', role: 'admin']
println "Last wins: ${a + b}"
Output
Merged: [theme:dark, language:en, fontSize:16] Config: [theme:dark, language:en, fontSize:16] After <<: [a:1, b:2, c:3, d:4] Spread merge: [x:1, y:2, z:3] Last wins: [name:Bob, role:admin]
What happened here: The + operator is the cleanest way to merge – it creates a new map with the right side taking precedence on conflicts. The spread operator *: lets you inline a map’s entries into a new map literal, which is great for combining multiple sources in one expression.
Example 8: Grouping with groupBy()
What we’re doing: Grouping a list of items into a map based on some criteria.
Example 8: groupBy
def people = [
[name: 'Alice', dept: 'Engineering', age: 30],
[name: 'Bob', dept: 'Marketing', age: 25],
[name: 'Charlie', dept: 'Engineering', age: 35],
[name: 'Diana', dept: 'Marketing', age: 28],
[name: 'Eve', dept: 'Engineering', age: 27]
]
// Group by department
def byDept = people.groupBy { it.dept }
byDept.each { dept, members ->
println "${dept}: ${members.collect { it.name }}"
}
println ""
// Group by age range
def byAgeGroup = people.groupBy { person ->
person.age < 30 ? 'Under 30' : '30 and over'
}
byAgeGroup.each { group, members ->
println "${group}: ${members.collect { it.name }}"
}
println ""
// countBy - like groupBy but returns counts
def countByDept = people.countBy { it.dept }
println "Count by dept: ${countByDept}"
Output
Engineering: [Alice, Charlie, Eve] Marketing: [Bob, Diana] 30 and over: [Alice, Charlie] Under 30: [Bob, Diana, Eve] Count by dept: [Engineering:3, Marketing:2]
What happened here: groupBy() is incredibly powerful – it takes a list and returns a map where keys are the grouping criteria and values are lists of matching items. Its cousin countBy() does the same but returns counts instead of lists. These two methods alone can replace dozens of lines of manual grouping code.
Example 9: Nested Maps
What we’re doing: Working with maps inside maps – a common pattern for JSON-like structures.
Example 9: Nested Maps
// JSON-like nested structure
def company = [
name: 'TechCorp',
address: [
street: '123 Main St',
city: 'San Francisco',
state: 'CA'
],
departments: [
engineering: [headcount: 50, budget: 5000000],
marketing: [headcount: 20, budget: 2000000]
]
]
// Access nested values with chained dot notation
println "Company: ${company.name}"
println "City: ${company.address.city}"
println "Eng budget: \$${company.departments.engineering.budget}"
// Safe navigation for potentially null paths
println "Missing: ${company?.finance?.budget}"
// Modify nested values
company.address.zip = '94105'
println "Added zip: ${company.address}"
// Flatten nested maps for display
company.departments.each { dept, info ->
println "${dept}: ${info.headcount} people, \$${info.budget} budget"
}
Output
Company: TechCorp City: San Francisco Eng budget: $5000000 Missing: null Added zip: [street:123 Main St, city:San Francisco, state:CA, zip:94105] engineering: 50 people, $5000000 budget marketing: 20 people, $2000000 budget
What happened here: Groovy’s dot notation chains beautifully for nested maps. The safe navigation operator ?. prevents NullPointerException when a middle level might be null. This pattern is how you’d model JSON data in Groovy – and with groovy.json.JsonSlurper, parsed JSON literally becomes nested maps and lists.
Example 10: Map Spread Operator
What we’re doing: Using the spread operator to apply operations across map values.
Example 10: Spread Operator
// Spread operator with maps inside a list
def employees = [
[name: 'Alice', salary: 85000],
[name: 'Bob', salary: 92000],
[name: 'Charlie', salary: 78000]
]
// Extract all names using spread-dot
def names = employees*.name
println "Names: ${names}"
// Extract all salaries
def salaries = employees*.salary
println "Salaries: ${salaries}"
println "Total payroll: \$${salaries.sum()}"
println "Average salary: \$${(salaries.sum() / salaries.size()).round(2)}"
// Spread operator in map literal construction
def base = [type: 'widget', color: 'blue']
def custom = [*:base, size: 'large', color: 'red']
println "Custom: ${custom}"
Output
Names: [Alice, Bob, Charlie] Salaries: [85000, 92000, 78000] Total payroll: $255000 Average salary: $85000.00 Custom: [type:widget, color:red, size:large]
What happened here: The spread-dot operator *. extracts a property from every element in a list of maps – it’s like calling collect { it.name } but shorter. The spread operator *: in map literals expands a map’s entries inline. Notice how color in the custom map overrides the one from base – later values win.
Example 11: Converting Between Maps and Lists
What we’re doing: Converting maps to lists and lists back to maps.
Example 11: Map-List Conversion
def fruits = [apple: 5, banana: 3, cherry: 8]
// Map to list of entries
def entryList = fruits.collect { k, v -> [k, v] }
println "Entry list: ${entryList}"
// Map to list of keys
println "Keys: ${fruits.keySet().toList()}"
// Map to list of values
println "Values: ${fruits.values().toList()}"
// List of pairs to map
def pairs = [['name', 'Alice'], ['age', 30], ['city', 'NYC']]
def fromPairs = pairs.collectEntries { [it[0], it[1]] }
println "From pairs: ${fromPairs}"
// Two parallel lists to map
def keys = ['a', 'b', 'c']
def values = [1, 2, 3]
def zipped = [keys, values].transpose().collectEntries { [it[0], it[1]] }
println "Zipped: ${zipped}"
// List to frequency map (counting occurrences)
def words = ['hello', 'world', 'hello', 'groovy', 'hello', 'world']
def frequency = words.countBy { it }
println "Word frequency: ${frequency}"
Output
Entry list: [[apple, 5], [banana, 3], [cherry, 8]] Keys: [apple, banana, cherry] Values: [5, 3, 8] From pairs: [name:Alice, age:30, city:NYC] Zipped: [a:1, b:2, c:3] Word frequency: [hello:3, world:2, groovy:1]
What happened here: The collectEntries method is the workhorse for building maps from lists. The transpose() trick zips two parallel lists together. And countBy is the simplest way to build a frequency map – one line instead of a manual loop with incrementing counters. For more on list operations, see our Groovy list tutorial and list to string conversion guide.
Example 12: Real-World – Config Maps, JSON, and Counting Occurrences
What we’re doing: Three real-world scenarios where maps are the natural solution.
Example 12: Real-World Map Usage
// Scenario 1: Application config map
def config = [
database: [
host: 'localhost',
port: 5432,
name: 'myapp_db'
],
cache: [
enabled: true,
ttl: 3600
],
features: [
darkMode: true,
beta: false
]
]
println "DB: ${config.database.host}:${config.database.port}/${config.database.name}"
println "Cache TTL: ${config.cache.ttl}s"
println "Dark mode: ${config.features.darkMode}"
// Scenario 2: Building a JSON-like response
def response = [
status: 200,
message: 'OK',
data: [
users: [
[id: 1, name: 'Alice'],
[id: 2, name: 'Bob']
],
total: 2
]
]
println "\nAPI Response status: ${response.status}"
println "Users: ${response.data.users*.name}"
// Scenario 3: Counting log event types
def logEvents = [
'ERROR', 'INFO', 'INFO', 'WARN', 'ERROR',
'INFO', 'INFO', 'ERROR', 'DEBUG', 'INFO'
]
def eventCounts = logEvents.countBy { it }.sort { a, b -> b.value <=> a.value }
println "\nLog summary:"
eventCounts.each { level, count ->
def bar = '#' * count
println " ${level.padRight(6)} ${bar} (${count})"
}
Output
DB: localhost:5432/myapp_db Cache TTL: 3600s Dark mode: true API Response status: 200 Users: [Alice, Bob] Log summary: INFO ##### (5) ERROR ### (3) WARN # (1) DEBUG # (1)
What happened here: These three scenarios cover the most common real-world uses of maps. Config maps with nested structure replace property files. Map literals mirror JSON so closely that transitioning between them is effortless. And the frequency counting pattern – countBy { it } – is a one-liner that replaces manual counter loops. This is where Groovy maps really shine.
Advanced Map Techniques
withDefault – Auto-Initializing Maps
One of the most useful Groovy map features is withDefault(). It creates a map that automatically generates values for missing keys:
withDefault
// Auto-initialize missing keys with a default value
def counter = [:].withDefault { 0 }
['apple', 'banana', 'apple', 'cherry', 'apple', 'banana'].each { fruit ->
counter[fruit]++
}
println "Counter: ${counter}"
// Auto-initialize with empty lists (great for grouping)
def groups = [:].withDefault { [] }
def students = [
[name: 'Alice', grade: 'A'],
[name: 'Bob', grade: 'B'],
[name: 'Charlie', grade: 'A'],
[name: 'Diana', grade: 'B'],
[name: 'Eve', grade: 'A']
]
students.each { s -> groups[s.grade] << s.name }
println "Groups: ${groups}"
Output
Counter: [apple:3, banana:2, cherry:1] Groups: [A:[Alice, Charlie, Eve], B:[Bob, Diana]]
withDefault eliminates the “check if key exists, if not initialize, then update” pattern that plagues Java code. The closure you pass determines what value to create for any key that doesn’t exist yet.
Submap and Inject
Submap and Inject
def user = [name: 'Alice', age: 30, email: 'alice@test.com', password: 'secret']
// subMap - extract specific keys
def publicInfo = user.subMap(['name', 'age', 'email'])
println "Public: ${publicInfo}"
// inject (fold/reduce) - compute aggregate from map
def prices = [apple: 2.50, banana: 1.75, cherry: 4.00]
def total = prices.inject(0) { sum, key, value -> sum + value }
println "Total: \$${total}"
// Build a query string from a map
def params = [page: 1, size: 20, sort: 'name']
def queryString = params.collect { k, v -> "${k}=${v}" }.join('&')
println "Query: ?${queryString}"
Output
Public: [name:Alice, age:30, email:alice@test.com] Total: $8.25 Query: ?page=1&size=20&sort=name
subMap() is perfect for extracting a subset of keys – great for stripping sensitive fields before returning data. inject() is Groovy’s version of reduce/fold – it accumulates a result across all entries. Building URL query strings from a parameter map is a pattern you’ll use over and over.
Deprecated vs Modern Approach
Old vs Modern Map Operations
def data = [name: 'Alice', age: 30, city: 'NYC']
// Old way: Java-style iteration
println "=== Java style ==="
def iterator = data.entrySet().iterator()
while (iterator.hasNext()) {
def entry = iterator.next()
println "${entry.key}: ${entry.value}"
}
// Modern way: Groovy each
println "\n=== Groovy each ==="
data.each { k, v -> println "${k}: ${v}" }
// Old way: Manual null checking for nested access
def config = [db: [host: 'localhost']]
def port = null
if (config.db != null) {
port = config.db.port != null ? config.db.port : 3306
}
println "\nOld port: ${port}"
// Modern way: Safe navigation + Elvis operator
def portModern = config?.db?.port ?: 3306
println "Modern port: ${portModern}"
// Old way: Manual grouping
def items = ['apple', 'avocado', 'banana', 'blueberry', 'cherry']
def grouped = new HashMap()
for (item in items) {
def key = item[0]
if (!grouped.containsKey(key)) grouped[key] = []
grouped[key].add(item)
}
println "\nOld grouped: ${grouped}"
// Modern way: groupBy
println "Modern grouped: ${items.groupBy { it[0] }}"
Output
=== Java style === name: Alice age: 30 city: NYC === Groovy each === name: Alice age: 30 city: NYC Old port: 3306 Modern port: 3306 Old grouped: [a:[apple, avocado], b:[banana, blueberry], c:[cherry]] Modern grouped: [a:[apple, avocado], b:[banana, blueberry], c:[cherry]]
Migration Note: Java-style iteration, manual null checking, and manual grouping all still work in Groovy. But
each(), the safe navigation operator?., the Elvis operator?:, andgroupBy()are more readable and less error-prone. Embrace the Groovy way.
Edge Cases and Best Practices
Best Practices Summary
DO:
- Use map literals
[key: value]instead ofnew HashMap() - Use
[:]for empty maps (notnew HashMap()) - Use parentheses
[(var): value]when you need a variable as a key - Use
withDefaultfor counter and grouping patterns - Use
toSorted { comparator }when you don’t want to mutate the original map - Prefer
?.keyfor potentially null maps to avoid NPE
DON’T:
- Use GStrings as map keys – they won’t match string keys on lookup
- Forget that
[]creates an empty list, not an empty map - Assume
sort()returns a new map – it modifies in place by default - Use
map.classto check the type –classis treated as a map key, usemap.getClass() - Modify a map while iterating over it with
each()– usefindAll()instead
Performance Considerations
For most applications, LinkedHashMap is perfectly fine. But here’s what to know when performance matters:
Performance Tips
// LinkedHashMap (default) - ordered, slightly more memory
def linked = [a: 1, b: 2, c: 3]
println "Default: ${linked.getClass().simpleName}"
// HashMap - unordered, slightly faster for large maps
def hash = new HashMap([a: 1, b: 2, c: 3])
println "HashMap: ${hash.getClass().simpleName}"
// TreeMap - sorted by key, O(log n) operations
def tree = new TreeMap([c: 3, a: 1, b: 2])
println "TreeMap: ${tree} (always sorted)"
// ConcurrentHashMap - thread-safe
def concurrent = new java.util.concurrent.ConcurrentHashMap()
concurrent.putAll([x: 1, y: 2])
println "Concurrent: ${concurrent.getClass().simpleName}"
// Benchmark tip: for 10K+ entries, consider HashMap over LinkedHashMap
// The ordering overhead is small but measurable at scale
println "\nTip: Use HashMap for large, unordered collections"
println "Tip: Use TreeMap when you need sorted keys"
println "Tip: Use ConcurrentHashMap for multi-threaded access"
Output
Default: LinkedHashMap HashMap: HashMap TreeMap: [a:1, b:2, c:3] (always sorted) Concurrent: ConcurrentHashMap Tip: Use HashMap for large, unordered collections Tip: Use TreeMap when you need sorted keys Tip: Use ConcurrentHashMap for multi-threaded access
The bottom line: LinkedHashMap (Groovy’s default) is fine for 99% of use cases. Switch to HashMap for large collections where order doesn’t matter, TreeMap when you need keys sorted, and ConcurrentHashMap for thread-safe access.
Common Pitfalls
Pitfall 1: The GString Key Trap
GString Key Pitfall
def key = 'name'
def map = [:]
map["${key}"] = 'Alice' // GString key
map[key] = 'Bob' // String key
println "Size: ${map.size()}" // 2! Not 1!
println "GString lookup: ${map["${key}"]}"
println "String lookup: ${map[key]}"
println "They're different: ${map}"
Output
Size: 2 GString lookup: Alice String lookup: Bob They're different: [name:Alice, name:Bob]
This is the number one map pitfall in Groovy. A GString and a String with the same content are not equal as map keys because they have different hashCode() values. Always use plain strings as map keys, or call .toString() on GStrings.
Pitfall 2: map.class Returns Null
map.class Pitfall
def map = [name: 'Alice']
// Wrong - 'class' is treated as a map key lookup!
println "map.class: ${map.class}" // null (no key called 'class')
// Correct - use getClass()
println "map.getClass(): ${map.getClass().simpleName}" // LinkedHashMap
Output
map.class: null map.getClass(): LinkedHashMap
Because maps use dot notation for key access, map.class looks up a key named “class” instead of returning the map’s class. Always use getClass() when checking a map’s type.
Pitfall 3: Variable Keys Need Parentheses
Variable Key Pitfall
def fieldName = 'email'
// Wrong - 'fieldName' becomes the literal string key
def wrong = [fieldName: 'alice@test.com']
println "Wrong: ${wrong}" // [fieldName:alice@test.com]
// Right - parentheses evaluate the variable
def right = [(fieldName): 'alice@test.com']
println "Right: ${right}" // [email:alice@test.com]
Output
Wrong: [fieldName:alice@test.com] Right: [email:alice@test.com]
Without parentheses, the identifier before the colon is always treated as a string literal. Wrap it in () to evaluate it as an expression. This catches even experienced Groovy developers sometimes.
Conclusion
We’ve covered a lot in this Groovy map tutorial – from basic creation with [:] and map literals to advanced techniques like groupBy(), withDefault(), spread operators, and nested map traversal. Maps are arguably the most versatile data structure in Groovy, and the GDK methods make them a joy to work with compared to Java.
The key things to remember: Groovy maps default to LinkedHashMap (insertion order preserved), keys are strings by default in literals, and dot notation gives you clean property-style access. Watch out for the GString key trap and the map.class gotcha – they’ve bitten every Groovy developer at least once.
For more specific map operations, check out our post on adding to maps in Groovy. And if you’re working with lists alongside maps, don’t miss the Groovy list tutorial.
Summary
- Groovy maps default to
LinkedHashMap– insertion order is always preserved - Map literal syntax
[key: value]is concise and readable – no need fornew HashMap() - Access values with dot notation (
map.key) or brackets (map['key']) - Use
collectEntries,groupBy,countBy, andfindAllfor powerful transformations - Never use GStrings as map keys – they don’t match String keys
- Use
[(variable): value]when you need a variable as a key in a map literal
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 Add to Map – All Methods Explained
Frequently Asked Questions
What is the default type of a Groovy map?
Groovy maps created with literal syntax like [key: value] default to java.util.LinkedHashMap. This means they preserve insertion order, unlike Java’s HashMap. You can explicitly use HashMap, TreeMap, or ConcurrentHashMap when you need different behavior.
How do I create an empty map in Groovy?
Use the [:] syntax to create an empty map. Don’t use [] – that creates an empty list, not a map. Example: def myMap = [:] creates an empty LinkedHashMap. This is one of the most common beginner mistakes in Groovy.
How do I use a variable as a map key in Groovy?
Wrap the variable in parentheses: [(myVariable): value]. Without parentheses, the identifier is treated as a string literal. For example, def key = ‘name’; def map = [(key): 'Alice'] creates [name: Alice], while [key: 'Alice'] creates a map with the literal string ‘key’ as the key.
Why can’t I use GStrings as map keys in Groovy?
GStrings and Strings have different hashCode() implementations, so a GString key and a String key with the same content are treated as different keys. This means map["${key}"] and map[key] access different entries. Always use plain String keys, or call .toString() on GStrings before using them as keys.
How do I iterate over a Groovy map?
Use the each() method with a two-parameter closure: map.each { key, value -> println “${key}: ${value}” }. You can also use eachWithIndex for numbered iteration, or iterate over keySet() and values() separately. For filtering during iteration, use findAll() instead of each().
Related Posts
Previous in Series: Groovy String Trim – Remove Whitespace Examples
Next in Series: Groovy Add to Map – All Methods Explained
Related Topics You Might Like:
- Groovy List Tutorial – The Complete Guide
- Groovy List to String – All Conversion Methods
- Groovy String Tutorial – The Complete Guide
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment