A map is a mapping from unique unordered keys to values:
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:
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.
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:
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:
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:
def m= [1:'a', 2:'b']
m.clear()
assert m == [:]
Further map methods:
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:
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:
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:
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:
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.
def family = [dad:"John" , mom:"Jane", son:"John"] def val = "John"
The simplest way to achieve this with the previous map:
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
def retVal = []
family.findAll{it.value == val}.each{retVal << it?.key}
assert retVal == ["son", "dad"]
If you just wanted the collection of Mappings:
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:
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:
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:
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:
//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:
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:
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:
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:
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.
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:
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:
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:
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:
// 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
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).
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)






