【问题标题】:Efficient algorithm for merging objects in ArrayList在 ArrayList 中合并对象的高效算法
【发布时间】:2015-08-03 09:15:10
【问题描述】:

我有一个自定义对象 (DTO) 的 ArrayList,DTO 的结构:

private String id;
private String text;
private String query;
private String locatorId;
private Collection<String> categories;
private Collection<String> triggers;

我有两个任务:

  • 删除 Array 中的重复项(看起来没问题,我应该使用 HashSet)
  • 在 ArrayList 中查找具有相同 id 字段的对象并将它们合并为一个对象(我应该合并字段类别和触发器)并使用合并的对象创建最终列表。

这种任务最有效的方法是什么?我也很有趣在我的算法中使用 Lambda 表达式。

【问题讨论】:

  • 如何合并文本、查询和类别?
  • 此字段不会被合并(它们将是相同的,只是字段类别和触发器不同)。

标签: java algorithm arraylist java-8


【解决方案1】:

使用流 API 按指定键合并对象非常容易。首先,在 Entity 类中定义一个 merge 方法,如下所示:

public Entity merge(Entity other) {
    this.categories.addAll(other.categories);
    this.triggers.addAll(other.triggers);
    return this;
}

然后你可以构建一个自定义的分组收集器:

import static java.util.stream.Collectors.*;

public static Collection<Entity> mergeAll(Collection<Entity> input) {
    return input.stream()
                .collect(groupingBy(Entity::getId,
                    collectingAndThen(reducing(Entity::merge), Optional::get)))
                .values();
}

这里我们根据getId方法的结果对Entity元素进行分组,下游收集器只在遇到相同的id时调用Entity.merge()(我们需要另外在Optional上展开)。在此解决方案中,Entity 不需要特殊的 hashCode()equals() 实现。

请注意,此解决方案会修改现有的未合并 Entity 对象。如果不受欢迎,请在 merge() 方法中创建一个新的 Entity 并返回它(如@Marco13 答案)。

【讨论】:

  • toMap(Entity::getId, e-&gt;e, Entity::merge) 替代groupingBy - reducing
  • @Misha,谢谢。可能我有太多的groupingBy 思维方式,总是忘记toMap 替代方案。
  • 还要注意,除非categoriestriggersSets,否则合并方法可能会在所述Collections 中生成重复条目,这可能不是 OP 通过“合并”的意思"。
  • @MickMnemonic,OP 已经同意使用HashSet 对他来说很好。
  • groupingBy 如果打开 Optional 不那么尴尬,也不会那么糟糕。我希望我们可以通过Collector 上的默认方法执行reducing(Entity::merge).andThen(Optional::get),这样它会从左到右而不是collectingAndThen 静态读取更自然。
【解决方案2】:

创建 Map&lt;Integer, DTO&gt; 并将您的 id 作为键和对象作为 DTO。在放入 map 之前,只需检查它是否已经包含该键,如果它确实包含该键,然后取出该键的 DTO 对象,并将类别和触发器与旧对象合并。

【讨论】:

    【解决方案3】:

    answer by Naman Gala 中所建议的,一种可能的解决方案是使用Map 从ID 到实体,并在实体具有相同ID 时手动合并实体。

    这是在mergeById 方法中实现的,其中有一些虚拟/示例输入

    • 必须合并两个实体(由于 ID 相同)
    • 两个实体相等(它们也将被“合并”,产生与其中一个输入相同的结果)

    .

    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.LinkedHashSet;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    
    
    public class MergeById
    {
        public static void main(String[] args)
        {
            List<Entity> entities = new ArrayList<Entity>();
            entities.add(new Entity("0", "A", "X", "-1", 
                Arrays.asList("C0", "C1"), Arrays.asList("T0", "T1")));
            entities.add(new Entity("0", "A", "X", "-1", 
                Arrays.asList("C2", "C3"), Arrays.asList("T2")));
            entities.add(new Entity("1", "B", "Y", "-2", 
                Arrays.asList("C0"), Arrays.asList("T0", "T1")));
            entities.add(new Entity("1", "B", "Y", "-2", 
                Arrays.asList("C0"), Arrays.asList("T0", "T1")));
            entities.add(new Entity("2", "C", "Z", "-3", 
                Arrays.asList("C0", "C1"), Arrays.asList("T1")));
    
            System.out.println("Before merge:");
            for (Entity entity : entities)
            {
                System.out.println(entity);
            }
    
            List<Entity> merged = mergeById(entities);
    
            System.out.println("After  merge:");
            for (Entity entity : merged)
            {
                System.out.println(entity);
            }
        }
    
        private static List<Entity> mergeById(Iterable<? extends Entity> entities)
        {
            Map<String, Entity> merged = new HashMap<String, Entity>();
            for (Entity entity : entities)
            {
                String id = entity.getId();
                Entity present = merged.get(id);
                if (present == null)
                {
                    merged.put(id, entity);
                }
                else
                {
                    merged.put(id, Entity.merge(present, entity));
                }
            }
            return new ArrayList<Entity>(merged.values());
        }
    
    }
    
    
    class Entity
    {
        private String id;
        private String text;
        private String query;
        private String locatorId;
        private Collection<String> categories;
        private Collection<String> triggers;
    
        Entity()
        {
            categories = new LinkedHashSet<String>();
            triggers = new LinkedHashSet<String>();
        }
    
        Entity(String id, String text, String query, String locatorId,
            Collection<String> categories, Collection<String> triggers)
        {
            this.id = id;
            this.text = text;
            this.query = query;
            this.locatorId = locatorId;
            this.categories = categories;
            this.triggers = triggers;
        }
    
        String getId()
        {
            return id;
        }
    
        static Entity merge(Entity e0, Entity e1)
        {
            if (!Objects.equals(e0.id, e1.id))
            {
                throw new IllegalArgumentException("Different id");
            }
            if (!Objects.equals(e0.text, e1.text))
            {
                throw new IllegalArgumentException("Different text");
            }
            if (!Objects.equals(e0.query, e1.query))
            {
                throw new IllegalArgumentException("Different query");
            }
            if (!Objects.equals(e0.locatorId, e1.locatorId))
            {
                throw new IllegalArgumentException("Different id");
            }
            Entity e = new Entity(e0.id, e0.text, e0.query, e0.locatorId, 
                new LinkedHashSet<String>(), new LinkedHashSet<String>());
            e.categories.addAll(e0.categories);
            e.categories.addAll(e1.categories);
            e.triggers.addAll(e0.triggers);
            e.triggers.addAll(e1.triggers);
            return e;
        }
    
        @Override
        public String toString()
        {
            return "Entity [id=" + id + ", text=" + text + ", query=" + query +
                ", locatorId=" + locatorId + ", categories=" + categories +
                ", triggers=" + triggers + "]";
        }
    
    }
    

    输出是

    Before merge:
    Entity [id=0, text=A, query=X, locatorId=-1, categories=[C0, C1], triggers=[T0, T1]]
    Entity [id=0, text=A, query=X, locatorId=-1, categories=[C2, C3], triggers=[T2]]
    Entity [id=1, text=B, query=Y, locatorId=-2, categories=[C0], triggers=[T0, T1]]
    Entity [id=1, text=B, query=Y, locatorId=-2, categories=[C0], triggers=[T0, T1]]
    Entity [id=2, text=C, query=Z, locatorId=-3, categories=[C0, C1], triggers=[T1]]
    After  merge:
    Entity [id=0, text=A, query=X, locatorId=-1, categories=[C0, C1, C2, C3], triggers=[T0, T1, T2]]
    Entity [id=1, text=B, query=Y, locatorId=-2, categories=[C0], triggers=[T0, T1]]
    Entity [id=2, text=C, query=Z, locatorId=-3, categories=[C0, C1], triggers=[T1]]
    

    关于使用 lambdas 执行此操作的请求:可能会编写一些棘手的 entities.stream().collect(...) 应用程序。但由于这不是问题的主要目标,我将把这部分答案留给其他人(但不会省略这个小提示:仅仅因为你可以并不意味着你必须这样做。有时,一个循环很好)。

    还请注意,这很容易概括,可能会从数据库中借用一些词汇。但我认为应该回答问题的要点。

    【讨论】:

    • 谢谢@Marco13,我会采用这种方法对我来说更有效(以后自己添加一些 Lambda 的东西)
    【解决方案4】:

    如果你坚持使用 lambda 表达式,你可以这样做:

    Set<X> x = new TreeSet<>((o1, o2) -> 
            ((X)o1).getId().equals(((X)o2).getId()) ? 0 : 1);
    
    List<X> list = new ArrayList<>(set.addAll(x));
    

    这将根据它们的 id 创建一个具有唯一对象的集合。接下来,对于list中的每个对象,从原始列表中找到对应的对象,合并内部集合。

    【讨论】:

      【解决方案5】:

      根据 DTO 中的 id 字段实现 equalshashCode,并将 DTO 存储在 Set 中。这应该可以解决您的两个问题;鉴于现在定义 DTO 相等性的方式,Set 中不能存在具有相同 id 的重复项。

      编辑:

      由于您的要求是基于新 DTO 的值合并现有 DTO 的类别和触发器,因此更适合存储 DTOs 的数据结构将是 Map&lt;DTO, DTO&gt;(因为检索元素很麻烦来自Set,一旦你把它们放进去)。另外,我认为DTO 中的类别和触发器应该定义为Sets,不允许重复;这将使合并操作更加简单:

      private Set<String> categories;
      private Set<String> triggers;
      

      假设DTO为上述字段提供访问器(getCategories/getTriggers)(并且这些字段永远不是null),现在可以通过以下方式实现合并:

      public static void mergeOrPut(Map<DTO,DTO> dtos, DTO dto) {
          if (dtos.containsKey(dto)) {
              DTO existing = dtos.get(dto);
              existing.getCategories().addAll(dto.getCategories());
              existing.getTriggers().addAll(dto.getTriggers());
          } else {
              dtos.put(dto, dto);
          }
      }
      

      上面的代码也可以很容易地修改为使用Map&lt;Integer, DTO&gt;,在这种情况下,您不需要在DTO 类中覆盖equalshashCode

      【讨论】:

      • 这不会合并具有相同 ID 的实体。它只会忽略其中一个 ID 会被重复的人。除此之外,我怀疑 hashCodeequals 仅基于 ID 的合理实现是可能的。
      • 同意 @Marco13 ,我不仅需要删除重复项,还需要合并具有相同 id 的对象。
      • @Marco13,仅基于 id 的 equals 实现非常有意义,例如当 DTO 对与数据库表中的行对应的实体建模时。
      • @Evgeniy,如果您需要合并实体,那么您需要告诉我们如何它应该发生。
      • @MickMnemonic 有人可能会争论这一点,但直观地说,当两个具有相同“ID”的对象具有不同的属性时,那么这个“ID”就不是一个“ID”,而只是一个任意值。 (但我知道,当提到数据库中 ID 的角色以及我不熟悉的“主键”等内容时,这种直觉可能是错误的......)
      猜你喜欢
      • 2019-09-17
      • 2011-05-27
      • 1970-01-01
      • 2018-01-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多