How to merge two maps in Groovy?
One of the most popular map-related operation in any programming language is merging two (or more) maps. In this short blog post, I explain how to do it in the Groovy programming language, starting from the simplest +
operation, up to more advanced use cases of merging nested maps and using runtime metaprogramming to add a merge
method to the Map
interface. Enjoy reading and learning!
Merge maps using +
operator
The easiest way to merge two maps in Groovy is to use +
operator. This method is straightforward - it creates a new map from the left-hand-side and right-hand-side maps. The below example illustrates it clearly.
def a = [a: 1, b: 3]
def b = [a: 2, c: 4]
def c = a + b
assert c == [a: 2, b: 3, c: 4]
However, this method has one limitation - it doesn’t perform a so-called "deep merge" operation. Any nested map won’t get merged - it will only override the left-hand-side map entry with the one from the right-hand-side map. Just like in the example shown below.
def a = [a: 1, b: 3, z: [a: 10, b: 20]]
def b = [a: 2, c: 4, z: [c: 30]]
def c = a + b
assert c == [a: 2, b: 3, c: 4, z: [c: 30]]
You can see that the final map contains the z
key. However, the value it stores comes from the right-hand-side map only.
Merge maps with nested maps
It can be fixed by implementing custom merge(lhs,rhs)
function.
def a = [a: 1, b: 3, z: [a: 10, b: 20]]
def b = [a: 2, c: 4, z: [c: 30]]
def merge(Map lhs, Map rhs) {
return rhs.inject(lhs.clone()) { map, entry ->
if (map[entry.key] instanceof Map && entry.value instanceof Map) {
map[entry.key] = merge(map[entry.key], entry.value)
} else {
map[entry.key] = entry.value
}
return map
}
}
def d = merge(a, b)
assert d == [a: 2, b: 3, c: 4, z: [a: 10, b: 20, c: 30]]
It starts with the inject
function, which is an equivalent (or close equivalent) of the reduce
or fold
functions from other popular programming languages. It takes two parameters - the initial value (the lhs.clone()
in our case), and the two-argument closure that receives the current accumulator (the map
variable from our example) and the current map entry from the iteration.
We use lhs.clone() to avoid creating any side effects. If we use just lhs , we would modify the state of the first map passed to the merge function. |
In the next step, we need to check what is the type of the specific map key. If the same key stores a map in both maps, we call a merge
function to create a new value as a result of merging two nested maps. Otherwise, we just copy the value from the right-hand-side map.
It looks like we are ready to go, but what if maps we are merging contain e.g. a list of numbers?
def a = [a: 1, b: 3, z: [a: 10, b: 20], y: [1,2,3,4]]
def b = [a: 2, c: 4, z: [c: 30], y: [5,6,7]]
def merge(Map lhs, Map rhs) {
return rhs.inject(lhs.clone()) { map, entry ->
if (map[entry.key] instanceof Map && entry.value instanceof Map) {
map[entry.key] = merge(map[entry.key], entry.value)
} else {
map[entry.key] = entry.value
}
return map
}
}
def d = merge(a, b)
assert d == [a: 2, b: 3, c: 4, z: [a: 10, b: 20, c: 30], y: [5,6,7]]
Our current implementation does not support merging nested lists. Let’s fix it.
Merging maps with nested lists
The solution to this problem is simple. We need to add one more condition.
def a = [a: 1, b: 3, z: [a: 10, b: 20], y: [1,2,3,4]]
def b = [a: 2, c: 4, z: [c: 30], y: [5,6,7]]
def merge(Map lhs, Map rhs) {
return rhs.inject(lhs.clone()) { map, entry ->
if (map[entry.key] instanceof Map && entry.value instanceof Map) {
map[entry.key] = merge(map[entry.key], entry.value)
} else if (map[entry.key] instanceof Collection && entry.value instanceof Collection) {
map[entry.key] += entry.value
} else {
map[entry.key] = entry.value
}
return map
}
}
def d = merge(a, b)
assert d == [a: 2, b: 3, c: 4, z: [a: 10, b: 20, c: 30], y: [1,2,3,4,5,6,7]]
It works!
Adding merge
method to the Map
interface
Groovy supports runtime and compile time metaprogramming. We can use it to add merge(Map m)
method to the Map
interface. Let’s re-use the merge
function we’ve already implemented.
def a = [a: 1, b: 3, z: [a: 10, b: 20], y: [1,2,3,4]]
def b = [a: 2, c: 4, z: [c: 30], y: [5,6,7]]
def merge(Map lhs, Map rhs) {
return rhs.inject(lhs.clone()) { map, entry ->
if (map[entry.key] instanceof Map && entry.value instanceof Map) {
map[entry.key] = merge(map[entry.key], entry.value)
} else if (map[entry.key] instanceof Collection && entry.value instanceof Collection) {
map[entry.key] += entry.value
} else {
map[entry.key] = entry.value
}
return map
}
}
Map.metaClass.merge << { Map rhs -> merge(delegate, rhs) } (1)
def e = a.merge(b) (2)
assert e == [a: 2, b: 3, c: 4, z: [a: 10, b: 20, c: 30], y: [1,2,3,4,5,6,7]]
In the final example, we use runtime metaprogramming to create merge(Map rhs)
method in the Map
interface . This way we can simply call a.merge(b)
to create a new map using deep merge operation.
Bonus: using Map.withDefault
method
The last tip comes from the Reddit user /u/-jp-
- thanks for the contribution! He posted a comment with an alternative solution that uses combination of Map.withDefault
method with a method pointer operator that creates a closure from the Map.get
method. It produces a slightly different solution than the one posted above. However, it’s very clever and creative way to use Groovy, so I want to share it with you as well. Here’s the original comment.
Depending on what semantics you want you can also use
withDefault
. This will create a view ofmap1
that gets missing references frommap2
:final map1 = [x: 1, y: 2] final map2 = [z: 3] final merged = map1.withDefault(map2.&get) println "$merged.x, $merged.y, $merged.z"
0 Comments