N级映射的递归合并

时间:2014-09-10 19:29:42

标签: java

有没有办法在Java中深度合并地图?我已经看过几篇关于它的帖子,但大多数似乎解决方案似乎只处理一个级别的合并或者是单调乏味的。

我的数据结构(使用JSON字符串表示地图)看起来与此类似:

{ name: "bob", emails: { home: "bob@home.com", work : "bob@work.com" } }

理想情况下,如果我有另一张地图,如

{ emails: { home2: "bob@home2.com" } } 

与第一张地图合并后,它看起来像

{ name: "bob", emails: { home: "bob@home.com", work : "bob@work.com", home2: "bob@home2.com } }

我可以保证我的所有地图都是<String, Object>。这是否有开箱即用的解决方案?我真的想避免自己为非常嵌套或大的对象编写一堆递归或迭代代码。

5 个答案:

答案 0 :(得分:14)

改进版:this gist

这是一种深度合并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;
}

适用于嵌套地图,对象和对象列表。享受。

(免责声明:我不是Java开发人员!)

答案 1 :(得分:4)

我使用jackson加载json和yaml配置文件,每个环境都有一个基本配置文件和一个配置文件。 我加载了基本配置和特定于环境的配置。 然后我深入合并了两张地图。列表也合并,删除重复。 值在map1上深度合并,map2中的值在发生碰撞时覆盖map1中的值。

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

例如: 基本配置:

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

测试env config:

electronics:
  computers:
    laptops:
      windows:
        surface: 2500
    desktop: 100
  phones:
    windows:
      nokia: 800

books:
  technical:
    - f sharp
  novels: [2666]
  poetry:
    - parra

合并配置:

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"

答案 2 :(得分:3)

我最近不得不做类似的事情,但是我想要一个解决方案:

  • 使用Java8
  • 不会修改原始地图,因此会返回新地图

merge方法执行实际的合并,它从获取原始地图的副本开始,然后递归合并嵌套的地图和集合。

如果发生键冲突,并且关联的值不是Map或Collection,则优先考虑Map b中的值。

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

这是复制地图的方法

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

和用于复制具有相同类型的Collection的帮助方法

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

答案 3 :(得分:2)

我真的很喜欢上面的奇迹。

我修改了一下:

  1. 性能稍高(通过使用Entry避免一些.get(_)调用)
  2. 递归任何收藏,而不仅仅是列表
  3. 一些更严格的验证
  4. 一般来说,这不是一个很好的方法,但我知道有时可能需要它。

    (p.s。我使用Guava,但是如果需要,这些调用(Objects.equals,Preconditions.checkArgument)很容易替换。)

        @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's 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);
        }
    }
    

答案 4 :(得分:-2)

我没有意识到的深度合并方法,但这很可能是设计上的。通常,嵌套的Map是一种反模式,因为它们很快就变得难以管理。例如,假设您提交了一段如下所示的代码:

Map<String, Map<String, Map<String, Object>>> someNonsensicalMap = someObject.getNonsensicalMap();

祝你好运,无需耗时的逆向工程工作,对地图所包含的内容有所了解。应该避免这种事情。

解决这个问题的常用习惯是使用类而不是嵌套映射来为内容提供一些有意义的上下文。在使用DOM解析XML时,就会发现一个真实的例子。看看这篇SO帖子的答案:parsing Xml with NodeList and DocumentBuilder。您可以看到,除了嵌套地图之外,还有一个NodeList对象,其中包含Element个,它们本身可以包含NodeList等等。这样更容易遵循并允许几乎无限的嵌套。

所以我强烈建议您重新访问您的设计并避免使用嵌套地图。使用类而不是嵌套映射时,可以向类中添加一个或多个合并方法来执行深度合并:

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

以上是基于您的JSON的一个简单示例,但可以轻松扩展为递归合并该类包含的任何字段。