A map is a mapping from unique unordered keys to values:
| Code Block |
|---|
|
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 |
|---|
|
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' ]
|
To use the value of a String as the key value of a map, simply surround the variable with parenthesis.
| Code Block |
|---|
|
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 |
|---|
|
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 |
|---|
|
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 |
|---|
|
def m= [1:'a', 2:'b']
m.clear()
assert m == [:]
|
Further map methods:
| Code Block |
|---|
|
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 |
|---|
|
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 |
|---|
|
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 |
|---|
|
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 |
|---|
|
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 |
|---|
|
def family = [dad:"John" , mom:"Jane", son:"John"]
def val = "John"
|
The simplest way to achieve this with the previous map:
| Code Block |
|---|
|
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 |
|---|
|
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 |
|---|
|
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 |
|---|
|
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 |
|---|
|
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 |
|---|
|
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 |
|---|
|
//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 |
|---|
|
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 |
|---|
|
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 |
|---|
|
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 |
|---|
|
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 |
|---|
|
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 |
|---|
|
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 |
|---|
|
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 |
|---|
|
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 |
|---|
|
// 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 |
|---|
|
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 |
|---|
|
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)
|