Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: GROOVY-6516

A map is a mapping from unique unordered keys to values:

Code Block
languagegroovy
def map= ['id':'FX-11', 'name':'Radish', 'no':1234, 99:'Y']
    //keys can be of any type, and mixed together; so can values
assert map == ['name':'Radish', 'id':'FX-11', 99:'Y', 'no':1234]
    //order of keys irrelevant
assert map.size() == 4
assert [1:'a', 2:'b', 1:'c' ] == [1:'c', 2:'b'] //keys unique

def map2= [
  'id': 'FX-17',
  name: 'Turnip', //string-keys that are valid identifiers need not be quoted
  99: 123, //any data can be a key
  (-97): 987, //keys with complex syntax must be parenthesized
  "tail's": true, //trailing comma OK
]

assert map2.id == 'FX-17'
    //we can use field syntax for keys that are valid identifiers
assert map2['id'] == 'FX-17' //we can always use subscript syntax
assert map2.getAt('id') == 'FX-17' //some alternative method names
assert map2.get('id') == 'FX-17'
assert map2['address'] == null //if key doesn't exist in map
assert map2.get('address', 'No fixed abode') == 'No fixed abode'
    //default value for non-existent keys

assert map2.class == null
    //field syntax always refers to value of key, even if it doesn't exist
//use getClass() instead of class for maps...
assert map2.getClass() == LinkedHashMap //the kind of Map being used

assert map2."tail's" == true
    //string-keys that aren't valid identifiers used as field by quoting them
assert ! map2.'99' && ! map2.'-97' //doesn't work for numbers, though

map2.name = 'Potato'
map2[-107] = 'washed, but not peeled'
map2.putAt('alias', 'Spud')
    //different alternative method names when assigning value
map2.put('address', 'underground')
assert map2.name == 'Potato' && map2[-107] == 'washed, but not peeled' &&
       map2.alias == 'Spud' && map2.address == 'underground'
assert map2 == [ id: 'FX-17', name: 'Potato', alias: 'Spud',
                 address: 'underground', 99: 123, (-97): 987,
                 (-107): 'washed, but not peeled', "tail's": true ]

def id= 'address'
def map3= [id: 11, (id): 22]
    //if we want a variable's value to become the key, we parenthesize it
assert map3 == [id: 11, address: 22]

It's a common idiom to construct an empty map and assign values:

Code Block
languagegroovy
def map4= [:]
map4[ 1 ]= 'a'
map4[ 2 ]= 'b'
map4[ true ]= 'p' //we can use boolean values as a key
map4[ false ]= 'q'
map4[ null ]= 'x' //we can also use null as a key
map4[ 'null' ]= 'z'
assert map4 == [1:'a', 2:'b', (true):'p', (false):'q', (null):'x', 'null':'z' ]

We can also use the '+' and '<<' operators to add elements to the map. Note that '<<' produces modifies the map while '+=' creates a new map:

Code Block
languagegroovy
def map4a = [:]
def map5a = map4a
assert map5a.is(map4a)
map4a << ['b':'b value']
assert map5a.is(map4a)
map4a+= ['a':'a value'] // map4a is not the original map4a anymore
assert !map5a.is(map4a)
assert map4a == [b:'b value', a:'a value']
assert map5a == [b:'b value']

To use the value of a String as the key value of a map, simply surround the variable with parenthesis. 

Code Block
languagegroovy
def foo = "test"
def map =  [(foo):"bar"]

println map // will output ["test":"bar"]
map = [foo:"bar"]
println map // will output ["foo":"bar"] 
 

 

We can use each() and eachWithIndex() to access keys and values:

Code Block
languagegroovy
def p= new StringBuffer()
[1:'a', 2:'b', 3:'c'].each{ p << it.key +': '+ it.value +'; ' }
    //we supply a closure with either 1 param...
assert p.toString() == '1: a; 2: b; 3: c; '

def q= new StringBuffer()
[1:'a', 2:'b', 3:'c'].each{ k, v-> q << k +': '+ v +'; ' } //...or 2 params
assert q.toString() == '1: a; 2: b; 3: c; '

def r= new StringBuffer()
[1:'a', 2:'b', 3:'c'].eachWithIndex{ it, i-> //eachIndex() always takes 2 params
  r << it.key +'('+ i +'): '+ it.value +'; '
}
assert r.toString() == '1(0): a; 2(1): b; 3(2): c; '

We can check the contents of a map with various methods:

Code Block
languagegroovy
assert [:].isEmpty()
assert ! [1:'a', 2:'b'].isEmpty()
assert [1:'a', 2:'b'].containsKey(2)
assert ! [1:'a', 2:'b'].containsKey(4)
assert [1:'a', 2:'b'].containsValue('b')
assert ! [1:'a', 2:'b'].containsValue('z')

We can clear a map:

Code Block
languagegroovy
def m= [1:'a', 2:'b']
m.clear()
assert m == [:]

Further map methods:

Code Block
languagegroovy
def defaults= [1:'a', 2:'b', 3:'c', 4:'d'], overrides= [2:'z', 5:'x', 13:'x']
def result= new HashMap(defaults)
result.putAll(overrides)
assert result == [1:'a', 2:'z', 3:'c', 4:'d', 5:'x', 13:'x']
result.remove(2)
assert result == [1:'a', 3:'c', 4:'d', 5:'x', 13:'x']
result.remove(2)
assert result == [1:'a', 3:'c', 4:'d', 5:'x', 13:'x']

...

We can inspect the keys, values, and entries in a view:

Code Block
languagegroovy
def m2= [1:'a', 2:'b', 3:'c']

def es=m2.entrySet()
es.each{
  assert it.key in [1,2,3]
  assert it.value in ['a','b','c']
  it.value *= 3 //change value in entry set...
}
assert m2 == [1:'aaa', 2:'bbb', 3:'ccc'] //...and backing map IS updated

def ks= m2.keySet()
assert ks == [1,2,3] as Set
ks.each{ it *= 2 } //change key...
assert m2 == [1:'aaa', 2:'bbb', 3:'ccc'] //...but backing map NOT updated
ks.remove( 2 ) //remove key...
assert m2 == [1:'aaa', 3:'ccc'] //...and backing map IS updated

def vals= m2.values()
assert vals.toList() == ['aaa', 'ccc']
vals.each{ it = it+'z' } //change value...
assert m2 == [1:'aaa', 3:'ccc'] //...but backing map NOT updated
vals.remove( 'aaa' ) //remove value...
assert m2 == [3:'ccc'] //...and backing map IS updated

vals.clear() //clear values...
assert m2 == [:] //...and backing map IS updated

assert es.is( m2.entrySet() ) //same instance always returned
assert ks.is( m2.keySet() )
assert vals.is( m2.values() )

We can use these views for various checks:

Code Block
languagegroovy
def m1= [1:'a', 3:'c', 5:'e'], m2= [1:'a', 5:'e']
assert m1.entrySet().containsAll(m2.entrySet())
    //true if m1 contains all of m2's mappings
def m3= [1:'g', 5:'z', 3:'x']
m1.keySet().equals(m3.keySet()) //true if maps contain mappings for same keys

These views also support the removeAll() and retainAll() operations:

Code Block
languagegroovy
def m= [1:'a', 2:'b', 3:'c', 4:'d', 5:'e']
m.keySet().retainAll( [2,3,4] as Set )
assert m == [2:'b', 3:'c', 4:'d']
m.values().removeAll( ['c','d','e'] as Set )
assert m == [2:'b']

Some more map operations:

Code Block
languagegroovy
def m= [1:'a', 2:'b', 3:'c', 4:'d', 5:'e']
assert [86: m, 99: 'end'].clone()[86].is( m ) //clone() makes a shallow copy

def c= []
def d= ['a', 'bb', 'ccc', 'dddd', 'eeeee']
assert m.collect{ it.value * it.key } == d
assert m.collect(c){ it.value * it.key } == d
assert c == d

assert m.findAll{ it.key == 2 || it.value == 'e' } == [2:'b', 5:'e']
def me= m.find{ it.key % 2 == 0 }
assert [me.key, me.value] in [ [2,'b'], [4,'d'] ]

assert m.toMapString() == '[1:"a", 2:"b", 3:"c", 4:"d", 5:"e"]'

def sm= m.subMap( [2,3,4] )
sm[3]= 'z'
assert sm == [2:'b', 3:'z', 4:'d']
assert m == [1:'a', 2:'b', 3:'c', 4:'d', 5:'e'] //backing map is not modified

assert m.every{ it.value.size() == 1 }
assert m.any{ it.key % 4 == 0 }

Getting Map key(s) from a value.

Code Block
languagegroovy
def family = [dad:"John" , mom:"Jane", son:"John"]

def val = "John"

The simplest way to achieve this with the previous map:

Code Block
languagegroovy
assert family.find{it.value == "John"}?.key == "dad"
//or
assert family.find{it.value == val}?.key == "dad"

...

This will place your results for the keys into a List of keys

Code Block
languagegroovy
def retVal = []
family.findAll{it.value == val}.each{retVal << it?.key}

assert retVal == ["son", "dad"]

If you just wanted the collection of Mappings:

Code Block
languagegroovy
assert family.findAll{it.value == val} == ["son":"John", "dad":"John"]
//or
def returnValue = family.findAll{it.value == val}
assert returnValue == ["son":"John", "dad":"John"]

...

We can use special notations to access all of a certain key in a list of similarly-keyed maps:

Code Block
languagegroovy
def x = [ ['a':11, 'b':12], ['a':21, 'b':22] ]
assert x.a == [11, 21] //GPath notation
assert x*.a == [11, 21] //spread dot notation

x = [ ['a':11, 'b':12], ['a':21, 'b':22], null ]
assert x*.a == [11, 21, null] //caters for null values
assert x*.a == x.collect{ it?.a } //equivalent notation
try{ x.a; assert 0 }catch(e){ assert e instanceof NullPointerException }
    //GPath doesn't cater for null values

class MyClass{ def getA(){ 'abc' } }
x = [ ['a':21, 'b':22], null, new MyClass() ]
assert x*.a == [21, null, 'abc'] //properties treated like map subscripting

def c1= new MyClass(), c2= new MyClass()
assert [c1, c2]*.getA() == [c1.getA(), c2.getA()]
    //spread dot also works for method calls
assert [c1, c2]*.getA() == ['abc', 'abc']

assert ['z':900, *:['a':100, 'b':200], 'a':300] == ['a':300, 'b':200, 'z':900]
    //spread map notation in map definition
assert [ *:[3:3, *:[5:5] ], 7:7] == [3:3, 5:5, 7:7]

def f(){ [ 1:'u', 2:'v', 3:'w' ] }
assert [*:f(), 10:'zz'] == [1:'u', 10:'zz', 2:'v', 3:'w']
    //spread map notation in function arguments
def f(m){ m.c }
assert f(*:['a':10, 'b':20, 'c':30], 'e':50) == 30

def f(m, i, j, k){ [m, i, j, k] }
    //using spread map notation with mixed unnamed and named arguments
assert f('e':100, *[4, 5], *:['a':10, 'b':20, 'c':30], 6) ==
    [ ["e":100, "b":20, "c":30, "a":10], 4, 5, 6 ]

...

We can group a list into a map using some criteria:

Code Block
languagegroovy
assert [ 'a', 7, 'b', [2,3] ].groupBy{ it.class } == [
  (String.class): ['a', 'b'],
  (Integer.class): [ 7 ],
  (ArrayList.class): [[2,3]]
]

assert [
  [name:'Clark', city:'London'], [name:'Sharma', city:'London'],
  [name:'Maradona', city:'LA'], [name:'Zhang', city:'HK'],
  [name:'Ali', city: 'HK'], [name:'Liu', city:'HK'],
].groupBy{ it.city } == [
  London: [ [name:'Clark', city:'London'],
            [name:'Sharma', city:'London'] ],
  LA: [ [name:'Maradona', city:'LA'] ],
  HK: [ [name:'Zhang', city:'HK'],
        [name:'Ali', city: 'HK'],
        [name:'Liu', city:'HK'] ],
]

By using groupBy() and findAll() on a list of similarly-keyed maps, we can emulate SQL:

Code Block
languagegroovy
assert ('The quick brown fox jumps over the lazy dog'.toList()*.
        toLowerCase() - ' ').
  findAll{ it in 'aeiou'.toList() }.
      //emulate SQL's WHERE clause with findAll() method
  groupBy{ it }.
      //emulate GROUP BY clause with groupBy() method
  findAll{ it.value.size() > 1 }.
      //emulate HAVING clause with findAll() method after the groupBy() one
  entrySet().sort{ it.key }.reverse().
      //emulate ORDER BY clause with sort() and reverse() methods
  collect{ "$it.key:${it.value.size()}" }.join(', ') == 'u:2, o:4, e:3'

An example with more than one "table" of data:

Code Block
languagegroovy
//find all letters in the "lazy dog" sentence appearing more often than those
//in the "liquor jugs" one...
def dogLetters= ('The quick brown fox jumps over the lazy dog'.toList()*.
                 toLowerCase() - ' '),
    jugLetters= ('Pack my box with five dozen liquor jugs'.toList()*.
                 toLowerCase() - ' ')
assert dogLetters.groupBy{ it }.
  findAll{   it.value.size() > jugLetters.groupBy{ it }[ it.key ].size()   }.
  entrySet().sort{it.key}.collect{ "$it.key:${it.value.size()}" }.join(', ') ==
      'e:3, h:2, o:4, r:2, t:2'

...

A HashMap is constructed in various ways:

Code Block
languagegroovy
def map1= new HashMap() //uses initial capacity of 16 and load factor of 0.75
def map2= new HashMap(25) //uses load factor of 0.75
def map3= new HashMap(25, 0.8f)
def map4= [:] //the shortcut syntax

...

A HashSet is implemented with a HashMap, and is constructed with the same choices of parameters:

Code Block
languagegroovy
def set1= new HashSet() //uses initial capacity of 16 and load factor of 0.75
def set2= new HashSet(25) //uses load factor of 0.75
def set3= new HashSet(25, 0.8f)
def set4= Collections.newSetFromMap( [:] )
    //we can supply our own empty map for the implementation

...

A sorted map is one with extra methods that utilize the sorting of the keys. Some constructors and methods:

Code Block
languagegroovy
def map= [3:'c', 2:'d' ,1:'e', 5:'a', 4:'b'], tm= new TreeMap(map)
assert tm.firstKey() == map.keySet().min() && tm.firstKey() == 1
assert tm.lastKey() == map.keySet().max() && tm.lastKey() == 5
assert tm.findIndexOf{ it.key==4 } == 3

We can construct a TreeMap by giving a comparator to order the elements in the map:

Code Block
languagegroovy
def c= [ compare:
  {a,b-> a.equals(b)? 0: Math.abs(a)<Math.abs(b)? -1: 1 }
] as Comparator

def tm= new TreeMap( c )
tm[3]= 'a'; tm[-7]= 'b'; tm[9]= 'c'; tm[-2]= 'd'; tm[-4]= 'e'
assert tm == new TreeMap( [(-2):'d', 3:'a', (-4):'e', (-7):'b', 9:'c'] )
assert tm.comparator() == c //retrieve the comparator

def tm2= new TreeMap( tm ) //use same map entries and comparator
assert tm2.comparator() == c

def tm3= new TreeMap( tm as HashMap )
    //special syntax to use same map entries but default comparator only
assert tm3.comparator() == null

The range-views, headMap() tailMap() and subMap(), are useful views of the items in a sorted map. They act similarly to the corresponding range-views in a sorted set.

Code Block
languagegroovy
def sm= new TreeMap(['a':1, 'b':2, 'c':3, 'd':4, 'e':5])
def hm= sm.headMap('c')
assert hm == new TreeMap(['a':1, 'b':2])
    //headMap() returns all elements with key < specified key
hm.remove('a')
assert sm == new TreeMap(['b':2, 'c':3, 'd':4, 'e':5])
    //headmap is simply a view of the data in sm
sm['a']= 1; sm['f']= 6
assert sm == new TreeMap(['a':1, 'b':2, 'c':3, 'd':4, 'e':5, 'f':6])
    //if backing sorted map changes, so do range-views
def tm= sm.tailMap('c')
assert tm == new TreeMap(['c':3, 'd':4, 'e':5, 'f':6])
    //tailMap() returns all elements with key >= specified element
def bm= sm.subMap('b','e')
assert bm == new TreeMap(['b':2, 'c':3, 'd':4])
    //subMap() returns all elements with key >= but < specified element
try{ bm['z']= 26; assert 0 }
catch(e){ assert e instanceof IllegalArgumentException }
    //attempt to insert an element out of range

...

We can convert a map into one that can't be modified:

Code Block
languagegroovy
def imMap= (['a':1, 'b':2, 'c':3] as Map).asImmutable()
try{ imMap['d']= 4; assert 0 }
catch(e){ assert e instanceof UnsupportedOperationException }
imMap= Collections.unmodifiableMap( ['a':1, 'b':2, 'c':3] as Map )
    //alternative way
try{ imMap['d']= 4; assert 0 }
catch(e){ assert e instanceof UnsupportedOperationException }

def imSortedMap= ( new TreeMap(['a':1, 'b':2, 'c':3]) ).asImmutable()
try{ imSortedMap['d']= 4; assert 0 }
catch(e){ assert e instanceof UnsupportedOperationException }
imSortedMap= Collections.unmodifiableSortedMap(
  new TreeMap(['a':1, 'b':2, 'c':3])
)   //alternative way
try{ imSortedMap['d']= 4; assert 0 }
catch(e){ assert e instanceof UnsupportedOperationException }

We can create an empty map that can't be modified:

Code Block
languagegroovy
def map= Collections.emptyMap()
assert map == [:]
try{ map['a']= 1; assert 0 }
catch(e){ assert e instanceof UnsupportedOperationException }
map= Collections.EMPTY_MAP
assert map == [:]
try{ map['a']= 1; assert 0 }
catch(e){ assert e instanceof UnsupportedOperationException }

We can create a single-element list that can't be modified:

Code Block
languagegroovy
def singMap = Collections.singletonMap('a', 1)
assert singMap == ['a': 1]
try{ singMap['b']= 2; assert 0 }
catch(e){ assert e instanceof UnsupportedOperationException }

...

We can convert a map into an observable one with the 'as' keyword too. An observable map will trigger a PropertyChangeEvent every time a value changes:

Code Block
languagegroovy
// don't forget the imports
import java.beans.*
def map = [:] as ObservableMap
map.addPropertyChangeListener({ evt ->
   println "${evt.propertyName}: ${evt.oldValue} -> ${evt.newValue}"
} as PropertyChangeListener)

map.key = 'value'  // prints key: null -> value
map.key = 'Groovy' // prints key: value -> Groovy

We can also wrap an existing map with an ObservableMap

Code Block
languagegroovy
import java.beans.*
def sorted = [a:1,b:2] as TreeMap
def map = new ObservableMap(sorted)
map.addPropertyChangeListener({ evt ->
   println "${evt.propertyName}: ${evt.oldValue} -> ${evt.newValue}"
} as PropertyChangeListener)
map.key = 'value'
assert ['a','b','key'] == (sorted.keySet() as List)
assert ['a','b','key'] == (map.keySet() as List)

Lastly we can specify a closure as an additional parameter, it will work like a filter for properties that should or should not trigger a PropertyChangeEvent when their values change, this is useful in conjunction with Expando. The filtering closure may take 2 parameters (the property name and its value) or less (the value of the property).

Code Block
languagegroovy
import java.beans.*
def map = new ObservableMap({!(it instanceof Closure)})
map.addPropertyChangeListener({ evt ->
   println "${evt.propertyName}: ${evt.oldValue} -> ${evt.newValue}"
} as PropertyChangeListener)
def bean = new Expando( map )
bean.lang = 'Groovy'  // prints lang: null -> Groovy
bean.sayHello = { name -> "Hello ${name}" } // prints nothing, event is skipped
assert 'Groovy' == bean.lang
assert 'Hello Groovy' == bean.sayHello(bean.lang)

Infinite nested maps tip

With the help of withDefault we can easily create inifinite nested maps structure :

Code Block
languagegroovy
Map infiniteNestedMaps = { [:].withDefault{ owner.call() } }()
        
infiniteNestedMaps.a.b.c.d.e.f.g = 42
        
assert infiniteNestedMaps.a.b.c.d.e.f.g == 42