Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Migrated to Confluence 5.3

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 a new map while '+' modifies the 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']

Great care must be exercised if mutable objects are used as map keys. The behavior of a map is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is a key in the map. A special case of this prohibition is that a map should not contain itself as a key.

Collection views of a map

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"

Note that the return is only the key dad. As you can see from the family Map both dad and son are keys for the same values.

So, let's get all of the keys with the value "John"
Basically, findAll returns a collection of Mappings with the value "John" that we then iterate through and print the key if the key is groovy true.

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"]

Special Notations

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 ]

Grouping

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'

HashMap Internals

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

The capacity is the number of buckets in the HashMap, and the initial capacity is the capacity when it's created. The load factor measures how full the HashMap will get before its capacity is automatically increased. When the number of entries exceeds the product of the load factor and the current capacity, the HashMap is rehashed so it has about twice the number of buckets. A HashMap gives constant-time performance for lookup (getting and putting). Iterating over collection views gives time performance proportional to the capacity of the HashMap instance plus its the number of keys. So don't set the initial capacity too high or the load factor too low. As a general rule, the default load factor (0.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost. Creating a HashMap with a sufficiently large capacity will allow mappings to be stored more efficiently than letting it perform automatic rehashing as needed to grow the table.

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

Sorted Maps

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

Immutable Maps

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 }

Observable Maps

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