Level up your Java skills with Apache Groovy Maps
Developers familiar with Java maps can benefit from exploring Apache Groovy maps. Groovy extends the functionality of Java maps by offering additional features, concise syntax and built-in methods for common operations. In turn, this can boost code readability and productivity while reducing boilerplate code. What’s more Groovy maps seamlessly integrate with existing Java code and their dynamic typing and suitability for specific use cases like configuration files and data pipelines solidify their value as a powerful tool for working with data structures in Java-based projects. (If you haven’t installed Groovy yet, please read the intro to this series.)
In my last article, I explored the enhancements and differences between lists in Java and Groovy. I also claimed that “lists are where Groovy starts to get way more interesting than the base functionality provided by the java.util.List interface.” I also claim that maps in Groovy are even more interesting in the way they extend and reinterpret java.util.Map. Let’s revisit some of the fundamental concepts of maps and then look at Groovy’s support for them.
First of all, Java (and Groovy) use the term “map” to indicate the definition of a relation between one kind of thing and another. Thus in Java programs, you see declarations such as Map<Integer,String> or, Map<String,MyClass>, or even Map<Integer,Map<String,MyClass>>. All of these are of the form:
Map<Key,Value>
This shows Key as being the class (or type) of keys, that is, the things being looked up. The Value is the class, (or type) of values associated with each key.
A good place to start learning Map in Groovy is to look at some basic syntactical elements. Here are a few, similar to the introduction to List in the previous article of this series:
1 def testMap = [:]
2 testMap[1] = 1
3 testMap[2] = "Hi there"
4 testMap[3] = java.time.LocalDate.of(2023,03,01)
5 testMap[15] = 2
6 testMap[-2] = 1.5
7 testMap.end = [3,4,5]
8 println "testMap $testMap"
9 println "testMap.class ${testMap.class}"
10 println "testMap.containsKey(2) ${testMap.containsKey(2)}"
Line 1 defines the variable testMap and sets it to the empty map.
Lines 2-4 insert values of an int (Integer), a String, and a java.time.LocalDate, at keys of lines one to three, demonstrating that Groovy is agnostic about map values of a certain type. This is a good moment to mention that testMap[1] = 1 is equivalent to testMap.put(1,1).
Line five inserts the int (Integer) value 2 in at key 15. Unlike in a list, this doesn’t have any “intermediate insertion” side-effects, because a map doesn’t have the concept of “missing keys”.
Line six uses a negative key, which, unlike Groovy lists, is not interpreted as being “from the right,” to insert the decimal (BigDecimal) value 1.5.
Line seven uses dot notation rather than a subscript to insert the value [3, 4, 5] (a List). This notation, testMap.end, is equivalent to testMap["end"] which is equivalent to testMap.get("end").
Line eight prints the list out and line nine tells you what sort of beast you’ve created.
Line 10 shows you the use of the containsKey() method to find out whether there is a specific key defined in the map. Recall that the in operator is used in a List instance to determine whether there is such a value in the list. Using in on a Map instance is almost always an error since it will return true if the tested value is either a key or a value.
Running this script yields:
$ groovy Groovy13a.groovy
testMap [1:1, 2:Hi there, 3:2023-03-01, 15:2, -2:1.5, end:[3, 4, 5]]
testMap.class null
testMap.containsKey(2) true
You should note that map entries are in order of insertion, not in some kind of key sort order. This is because the [:] creates an instance of LinkedHashMap. Note as well that testMap.class yields the surprising result of null. Any guesses? Would it help if I mentioned that testMap.end is equivalent to testMap["end"] which is equivalent to testMap.get("end")? To find out the class of the map you need to use instead: use the direct call to getClass(). That will tell you that the map is an instance of java.util.LinkedHashMap.
As in the case of the List interface, Groovy adds many methods to the Map interface. An important thing to keep in mind is that some of these methods mutate the map and others create a new map from the old methods. This happens because modifications are applied to the methods.
For example, plus() (which can be accessed by the + operator) creates a new map that is like a “union” of the two maps: preserving the order of the keys. Using minus() produces a new map that shows the “difference” between the two maps. This occurs by removing all elements of the second map from the first:
1 def m1 = [1:1, 2:2, 3:3]
2 def m2 = [4:4, 5:5]
3 def m3 = [1:6, 6:6]
4 println "m1 $m1"
5 println "m2 $m2"
6 println "m3 $m3"
7 println "m1 + m2 ${m1 + m2}"
8 println "m1 + m3 ${m1 + m3}"
9 println "m1 - m2 ${m1 - m2}"
10 println "m1 - m3 ${m1 - m3}"
Lines one to three create three maps m1, m2, and m3 . Note that the first key of m1 is equal to the first key of m3 but their values are different.
Line 4-6 print those maps out: no surprises there.
Lines 7-8 illustrate the + operator (which is the plus() method) to create two new maps, one being the “union” of m1 and m2, the other of m1 and m3.
Lines nine to 10 illustrate the - operator (which is the minus() method) to create the “difference” between the two maps.
Let’s see what happens when you run this:
$ groovy Groovy13b.groovy
m1 [1:1, 2:2, 3:3]
m2 [4:4, 5:5]
m3 [1:6, 6:6]
m1 + m2 [1:1, 2:2, 3:3, 4:4, 5:5]
m1 + m3 [1:6, 2:2, 3:3, 6:6]
m1 - m2 [1:1, 2:2, 3:3]
m1 - m3 [1:1, 2:2, 3:3]
I would like to point out a couple of things.
First, the concept of “union” needs to be considered in the context of what a “set union” means. Map keys form a set — there are no duplicate keys in a map. So the union of m1 and m3 results in the value of key 1 in m3 replacing the value of key 1 in m1. This makes sense.
Second, the difference between m1 and m3 doesn’t eliminate the entry 1:1 from m1. Java (and therefore Groovy), instances of Map contain instances of MapEntry. It appears that minus() — the difference operator — only removes a MapEntry from the first map that is equal to a MapEntry in the second. Returning to the union operation, you can infer that the MapEntry 1:1 in m1 is replaced by the MapEntry 1:6 from m3, because the keys are equal.
Some other interesting methods Groovy defines for Map include:
m1.asImmutable(): Creates an immutable copy ofm1(same asasUnmodifiable())m1.drop(n): Creates a copy ofm1without the firstnMapEntriesm1.take(n): Creates a copy ofm1containing only the firstnMapEntryinstances;m1.intersect(m2): Creates a new map containingMapEntryinstances found in bothm1andm2m1.sort(): Reordersm1in order of the keys (note this can yield some strange results when the keys are of differing types)m1.toSorted(): Creates a copy ofm1sorted in order of the keys
As with List, note that there are methods that produce altered copies of the Map instance. Some methods alter the Map instance itself. In hindsight, it would have been wise to have a consistent naming convention to draw attention to the difference.
Many of the cool methods added to the Map interface take a Closure parameter to do interesting things to the list. You will see some of these things in an upcoming article on closures.
This is a good point to suggest a diversion to the Groovy Map interface reference description and also a read of the Groovy documentation on Map.
Conclusion
Groovy elevates the pedestrian Java Map implementations to greatness. It does this by adding many useful methods and providing operators and other syntactic support that use some of those common methods in a more readable and compact fashion.
One main difference from Java is Groovy doesn’t operate from the point of view that Map instances should contain elements of the same type. Without type-checking enabled, Groovy doesn’t enforce the element type. For instance:
1 def rn = new LinkedHashMap<Integer,String>()
2 rn[1] = "i"
3 rn[2] = "ii"
4 rn[3] = "iii"
5 rn["4"] = "iv"
6 println rn
This compiles and executes just fine despite the string key “4”:
$ groovy Groovy13c.groovy
[1:i, 2:ii, 3:iii, 4:iv]
Together with the syntactic use of dot instead of brackets, the implication of this is quite interesting. One use of Map in Groovy acts like a “dynamic structure”, replacing the use of a class definition. That might seem kind of ugly to some readers. But in the right context, it can produce compact and much more readable code without tens of extra lines of class definition (or even Java’s new record definition) around.
Stay tuned for the next tutorial on how to get closure with Groovy.
