Groovy from the ground up

Lists and maps

Square brackets give you both of Groovy's everyday collections: an ordered list you can grow, and a map of names to values that keeps its keys.

Part 4 of 12

Beginner16 min readBack to Courses

A list is a row of values

def fruits = ['apple', 'pear', 'plum'] makes a list. It is ordered, it keeps duplicates, and it can be added to: fruits << 'fig' appends. Reading a position uses square brackets and counts from zero, so fruits[0] is the first item — and a negative index counts from the end, which makes fruits[-1] the last item without asking for the size first.

The list you get is an ArrayList underneath, which is why the usual methods are there: size(), contains(), remove(), indexOf(). Lists are also what a great deal of Groovy returns; split answers a list, keySet() answers a set, and the closure methods in a later chapter all answer lists. Because the backing store is an array, adding at the front shifts every later item along by one; appending with << does not, which is why building a list by appending is cheap and inserting at the front is not.

A map is a set of names for values

def scores = [ada: 42, grace: 7] makes a map, with : between a key and its value. A key written without quotes is a string, so scores['ada'] and scores.ada both read 42 — the dot is the short form when the key is a name you could type as an identifier. The two forms are not quite equal: the unquoted dot needs something that looks like a variable name, so a key with a space or a dash in it has to go in brackets.

A missing key answers null rather than failing, which is convenient and quietly dangerous: scores['alan'] is null, so code that does arithmetic with it fails later, further away from the real mistake. containsKey is how you ask whether the name is there at all before using it.

They keep their order, and they nest

Both collections keep the order you put things in, and printing either shows that order: a list prints as [apple, pear] and a map as [ada:42, grace:7]. That is comfortable and worth relying on — Groovy's literal maps are LinkedHashMaps.

Nesting is where they earn their keep. A list of maps is a table, with one map per row and one key per column, and that one structure is behind almost every report in this course: read a row, ask for a field, keep the rows that matter.

Growing, reading and nesting

def queue = ['Ada', 'Grace', 'Linus']
println queue[1]
println queue.size()
queue << 'Barbara'
queue.remove('Grace')
println queue

def ages = [Ada: 36, Grace: 45]
println ages['Ada']
println ages.containsKey('Linus')
ages.Linus = 55
println ages.keySet()

// A list of maps is a table.
def orders = [
    [item: 'pen', qty: 2],
    [item: 'pad', qty: 5],
]
println orders[1].item
println orders.collect { it.qty }.sum()

Indexes, slices and two kinds of remove

Square brackets do more than read one item. fruits[1..2] answers a new list with the items at those positions, and fruits[0] = 'fig' replaces one. Negative indexes count from the end, so fruits[-2..-1] is the last two items, and a range that runs backwards answers them in reverse.

remove is the method to read twice. remove(1) takes the item at index 1; remove('pear') takes the item equal to that text. A list of numbers makes the ambiguity real: remove(2) removes the third item, not the value 2. When the intent is a value, say so with removeElement or find the index first. The symptom of the wrong choice is a list that shrank by one but lost the wrong item, and the mistake usually surfaces later as a value that is missing rather than as an exception.

Maps: keys, defaults and iteration

A map literal is a LinkedHashMap, which is why printing keeps your order. Keys are unique: assigning scores.ada = 50 replaces the value rather than adding a second entry. Reading a missing key answers null, and null propagates quietly until something calls a method on it.

get(key, fallback) is the safe read: it answers the fallback without adding the key. containsKey answers the question directly, and each { key, value -> ... } walks entries in order. collectEntries builds a new map from a list, which is how one shape of data becomes another without a loop.

Three collection mistakes: remove(1) on a list removes by index, while remove('x') removes by value; def copy = original gives one collection two names, so changes through either are visible through both; and a missing map key is null, so map[key] + 1 throws instead of answering 1. Use get(key, 0) when a default is what you mean.

Try these next

  • Take a slice of the middle of a list and print it without changing the original.
  • Build a map of names to scores, then read a missing name with get and a fallback.
  • Turn a list of maps into a map of item to quantity with collectEntries.

One shopping list, one price list

Run it and follow the list as it grows. Then add a fruit, read the second item, and add a new key to the map and print the whole map.

Output
Click "Run" to execute your code

A small inventory

Run it and follow the quantity change. Then add a fifth item, take a slice of the first three names, and read a missing item from the map with a fallback of 0.

Output
Click "Run" to execute your code

<< changes the list it belongs to; + answers a new list and leaves the original alone. The difference matters the first time you "copy" a list with = — that is the same list under two names.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does fruits[-1] give you?