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.

+3


source to share


4 answers


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!)

+10


source


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"

      

+3


source


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);
    }
}

      

+2


source


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.

-1


source







All Articles