Is there a way to deep a deep merge of maps in Java? I've seen a couple posts about it but most seem solutions seem to only deal with one level of merging or are of tedious.
My data structure (using a JSON string to represent the map) looks something similar to this:
{ name: "bob", emails: { home: "[email protected]", work : "[email protected]" } }
Ideally if I have another map like
{ emails: { home2: "[email protected]" } }
post merge with the first map it would look something like
{ name: "bob", emails: { home: "[email protected]", work : "[email protected]", home2: "[email protected] } }
I can guarantee all of my Maps are of <String, Object>
. Is there an out of the box solution to this? Really am trying to avoid self writing a bunch of recursive or iterative code for very nested or large objects.
Improved version of: this gist
Here is a way to deep merge Java Maps:
// This is fancier than Map.putAll(Map)
private static Map deepMerge(Map original, Map newMap) {
for (Object key : newMap.keySet()) {
if (newMap.get(key) instanceof Map && original.get(key) instanceof Map) {
Map originalChild = (Map) original.get(key);
Map newChild = (Map) newMap.get(key);
original.put(key, deepMerge(originalChild, newChild));
} else if (newMap.get(key) instanceof List && original.get(key) instanceof List) {
List originalChild = (List) original.get(key);
List newChild = (List) newMap.get(key);
for (Object each : newChild) {
if (!originalChild.contains(each)) {
originalChild.add(each);
}
}
} else {
original.put(key, newMap.get(key));
}
}
return original;
}
Works for nested maps, objects, and lists of objects. Enjoy.
(Disclaimer: I am no Java developer!)
I am using jackson to load json and yaml configuration files, there is a base configuration file and one configuration file for each environment. I load the base config and the environment specific config. Then I deep merge both maps. Lists are also merged, removing duplicates. Values are deep merged on map1 and values from map2 override values from map1 in case of collisions.
void deepMerge(Map<String, Object> map1, Map<String, Object> map2) {
for(String key : map2.keySet()) {
Object value2 = map2.get(key);
if (map1.containsKey(key)) {
Object value1 = map1.get(key);
if (value1 instanceof Map && value2 instanceof Map)
deepMerge((Map<String, Object>) value1, (Map<String, Object>) value2);
else if (value1 instanceof List && value2 instanceof List)
map1.put(key, merge((List) value1, (List) value2));
else map1.put(key, value2);
} else map1.put(key, value2);
}
}
List merge(List list1, List list2) {
list2.removeAll(list1);
list1.addAll(list2);
return list1;
}
For example: Base config:
electronics:
computers:
laptops:
apple:
macbook: 1000
macbookpro: 2000
windows:
surface: 2000
desktop:
apple:
imac: 1000
windows:
surface: 2000
phones:
android:
samsung:
motox: 300
apple:
iphone7: 500
books:
technical:
- java
- perl
novels:
- guerra y paz
- crimen y castigo
poetry:
- neruda
- parra
test env config:
electronics:
computers:
laptops:
windows:
surface: 2500
desktop: 100
phones:
windows:
nokia: 800
books:
technical:
- f sharp
novels: [2666]
poetry:
- parra
merged config:
electronics:
computers:
laptops:
apple:
macbook: 1000
macbookpro: 2000
windows:
surface: 2500
desktop: 100
phones:
android:
samsung:
motox: 300
apple:
iphone7: 500
windows:
nokia: 800
books:
technical:
- "java"
- "perl"
- "f sharp"
novels:
- "guerra y paz"
- "crimen y castigo"
- 2666
poetry:
- "neruda"
- "parra"
I recentlty had to do something similar, but i wanted a solution:
The merge method does the actual merge, it starts with getting a copy of the original map and recursively merges nested maps and collections.
If there are key collisions and the associated values are not a Map or a Collection, values from Map b are given the priority.
public static Map merge(Map a, Map b) {
if (a == null && b == null)
return null;
if (a == null || a.size() == 0)
return copy(b);
if (b == null || b.size() == 0)
return copy(a);
Map copy = copy(a);
copy.putAll(
(Map) b
.keySet()
.stream()
.collect(
Collectors.toMap(
key -> key,
key -> {
Object original = copy.get(key);
Object value = b.get(key);
if (value == null && original == null)
return null;
if (value == null && original != null)
return original;
if (value != null && original == null)
return value;
if (value instanceof Map && original instanceof Map)
return merge((Map) original, (Map) value);
else if (value instanceof Collection
&& original instanceof Collection) {
try {
Collection merge =
newCollectionInstance(
(Collection) original,
(List) Lists
.newArrayList(
(Collection) original,
(Collection) value)
.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList()));
return merge;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return value;
})));
return copy;
}
Here is the method to copy a map
public static Map copy(Map original) {
return (Map) original
.keySet()
.stream()
.collect(
Collectors.toMap(
key -> key,
key -> {
Object value = original.get(key);
if (value instanceof Map)
return copy((Map) value);
if (value instanceof Collection)
return newCollectionInstance((Collection) value, (Collection) value);
return value;
}));
}
and a helper method to copy a Collection with the same type
public static Collection newCollectionInstance(Collection collection, Collection elements) {
try {
Collection newInstance = collection.getClass().newInstance();
newInstance.addAll(elements);
return newInstance;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With