Recursive fusion of N maps
Is there a way to deeply merge maps in Java? I've seen a couple of posts about this, but most of the seeming solutions seem to only deal with one level of merge, or tedious.
My data structure (using a JSON string to represent the map) looks something like this:
{ name: "bob", emails: { home: "bob@home.com", work : "bob@work.com" } }
Ideally, if I have another card like
{ emails: { home2: "bob@home2.com" } }
post merge with the first map will look something like this:
{ name: "bob", emails: { home: "bob@home.com", work : "bob@work.com", home2: "bob@home2.com } }
I can guarantee that all my Cards are relevant <String, Object>
. Is there a solution out of the box? I actually try not to write myself a bunch of recursive or iterative code for very nested or large objects.
source to share
Improved version: this meaning
Here's 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 object lists. Enjoy.
(Disclaimer: I'm not a Java developer!)
source to share
I am using jackson to load json and yaml , there is a base config file and one config file for each environment. I am downloading a basic and environment specific configuration. Then I deeply combine both cards. Lists are also merged by removing duplicates. The values ββare deeply merged on map1 and the values ββfrom map2 override the 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: Basic configuration:
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"
source to share
I really like wonderkid2's approach above.
I changed it a bit:
- slightly better performance (excluding some .get (_) calls with Entry)
- recurse on any collection, not just lists
- some more stringent checks
It's not a great approach overall, but I can see how it might be needed sometimes.
(ps I am using Guava, but these calls (Objects.equals, Preconditions.checkArgument) are easily replaced as needed.)
@SuppressWarnings({"rawtypes", "unchecked"})
static void deepMerge(
Map original,
Map newMap) {
for (Entry e : (Set<Entry>) newMap.entrySet()) {
Object
key = e.getKey(),
value = e.getValue();
// unfortunately, if null-values are allowed,
// we suffer the performance hit of double-lookup
if (original.containsKey(key)) {
Object originalValue = original.get(key);
if (Objects.equal(originalValue, value))
return;
if (originalValue instanceof Collection) {
// this could be relaxed to simply to simply add instead of addAll
// IF it not a collection (still addAll if it is),
// this would be a useful approach, but uncomfortably inconsistent, algebraically
Preconditions.checkArgument(value instanceof Collection,
"a non-collection collided with a collection: %s%n\t%s",
value, originalValue);
((Collection) originalValue).addAll((Collection) value);
return;
}
if (originalValue instanceof Map) {
Preconditions.checkArgument(value instanceof Map,
"a non-map collided with a map: %s%n\t%s",
value, originalValue);
deepMerge((Map) originalValue, (Map) value);
return;
}
throw new IllegalArgumentException(String.format(
"collision detected: %s%n%\torig:%s",
value, originalValue));
} else
original.put(key, value);
}
}
source to share
There is no deep merging method that I am aware of, but this is probably by design. In general, nested ones Map
are anti-patterns as they quickly become unmanageable. For example, suppose you are given a piece of code that looks like this:
Map<String, Map<String, Map<String, Object>>> someNonsensicalMap = someObject.getNonsensicalMap();
Itβs a good luck to understand what this map contains without the time-consuming reverse engineering effort. This should be avoided.
A common idiom to fix this is to use a class instead of a nested map to give the content some meaningful context. One real-world example of this comes into play when parsing XML with the DOM. Have a look at the answer to this SO post: Parsing Xml with NodeList and DocumentBuilder . You can see that instead of nested maps, there is an object NodeList
that contains Element
s, that can contain NodeList
s, etc. It's easier to follow and allow for almost infinite nesting.
Therefore, I highly recommend revisiting your design and avoiding nested cards. When using classes instead of a nested map, you can add a merge method or methods to your class to perform deep merging:
class Person {
Set<String> names = new HashSet<String>();
Set<String> emails = new HashSet<String>();
public Set<String> getNames() { return names; }
public Set<String> getEmails() { return emails; }
public void mergeNames(HashSet<String> moreNames) {
names.addAll(moreNames);
}
public void mergeEmails(HashSet<String> moreEmails) {
emails.addAll(moreEmails);
}
public void mergeNames(HashSet<String> moreNames) {
names.addAll(moreNames);
}
public void mergePerson(Person person) {
mergeNames(person.getNames());
mergeEmails(person.getEmails());
}
}
The above example is a trivial example based on your JSON above, but can easily be extended to recursively concatenate all the fields the class contains.
source to share