【问题标题】:Merging lists under same objects in a list using Java streams使用Java流合并列表中相同对象下的列表
【发布时间】:2018-03-05 21:19:32
【问题描述】:

我有两个对象,如下所示:

public class A {
    private Integer id;
    private String name;
    private List<B> list;

    public A(Integer id, String name, List<B> list) {
        this.id = id;
        this.name = name;
        this.list = list;
    }

    //getters and setters
}

public class B {
    private Integer id;
    private String name;

    public B(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    //getters and setters
}

所以,A 持有 B 的列表,并且 A 的列表填充如下:

    List<A> list = new ArrayList<>();
    list.add(new A(1, "a_one", Arrays.asList(new B(1, "b_one"), new B(2, "b_two"))));
    list.add(new A(2, "a_two", Arrays.asList(new B(2, "b_two"))));
    list.add(new A(1, "a_one", Arrays.asList(new B(3, "b_three"))));
    list.add(new A(2, "a_two", Arrays.asList(new B(4, "b_four"), new B(5, "b_five"))));
    list.add(new A(3, "a_three", Arrays.asList(new B(4, "b_four"), new B(5, "b_five"))));

我想通过合并具有相同 id 的 A 对象来获取一个新列表。结果列表必须是这样的:

[
    A(1, a_one, [B(1, b_one), B(2, b_two), B(3, b_three)]),
    A(2, a_two, [B(2, b_two), B(4, b_four), B(5, b_five)]),
    A(3, a_three, [B(4, b_four), B(5, b_five)])
]

我确实设法将列表与以下代码合并:

List<A> resultList = new ArrayList<>();
list.forEach(a -> {
    if (resultList.stream().noneMatch(ai -> ai.getId().equals(a.getId()))) {
        a.setList(list.stream().filter(ai -> ai.getId().equals(a.getId()))
                .flatMap(ai -> ai.getList().stream()).collect(Collectors.toList()));
        resultList.add(a);
    }
});

我的问题是,有没有合适的方法通过使用流收集器来做到这一点?

【问题讨论】:

  • 应该有正确的方法

标签: java java-8 java-stream collectors


【解决方案1】:

假设类 A 有一个复制构造函数可以有效地复制 List&lt;B&gt; list 属性和一个合并两个 A 实例的方法:

public A(A another) {
    this.id = another.id;
    this.name = another.name;
    this.list = new ArrayList<>(another.list);
}

public A merge(A another) {
    list.addAll(another.list):
    return this;
}

您可以通过以下方式实现您想要的:

Map<Integer, A> result = listOfA.stream()
    .collect(Collectors.toMap(A::getId, A::new, A::merge));

Collection<A> result = map.values();

这使用Collectors.toMap,它需要一个从流的元素中提取映射键的函数(这里是A::getId,它提取Aid),这个函数将流的每个元素转换为映射的值(此处为 A::new,它引用复制构造函数)和一个合并函数,该函数将映射的两个具有相同键的值(此处为 @987654332 @,仅当映射已经包含相同键的条目时才调用)。

如果您需要 List&lt;A&gt; 而不是 Collection&lt;A&gt;,只需执行以下操作:

List<A> result = new ArrayList<>(map.values());

【讨论】:

    【解决方案2】:

    如果您不想使用额外的功能,您可以执行以下操作,它可读且易于理解,首先按 id 分组,使用列表中的第一个元素创建一个新对象,然后将 B 的所有类加入到终于拿到A了。

    List<A> result = list.stream()
        .collect(Collectors.groupingBy(A::getId))
        .values().stream()
        .map(grouped -> new A(grouped.get(0).getId(), grouped.get(0).getName(),
                grouped.stream().map(A::getList).flatMap(List::stream)
                    .collect(Collectors.toList())))
        .collect(Collectors.toList());
    

    另一种方法是使用二元运算符和Collectors.groupingBy 方法。此处您使用 java 8 可选类在 fst 为空时第一次创建新的 A。

    BinaryOperator<A> joiner = (fst, snd) -> Optional.ofNullable(fst)
        .map(cur -> { cur.getList().addAll(snd.getList()); return cur; })
        .orElseGet(() -> new A(snd.getId(), snd.getName(), new ArrayList<>(snd.getList())));
    
    Collection<A> result = list.stream()
        .collect(Collectors.groupingBy(A::getId, Collectors.reducing(null, joiner)))
        .values();
    

    如果您不喜欢在简短的 lambda 中使用 return(看起来不太好),唯一的选择是过滤器,因为 java 不提供像 stream's peek 这样的另一种方法(注意:一些 IDE 突出显示以“简化”不应该在过滤器中进行表达和突变[但我认为在地图中也不应该])。

    BinaryOperator<A> joiner = (fst, snd) -> Optional.ofNullable(fst)
        .filter(cur -> cur.getList().addAll(snd.getList()) || true)
        .orElseGet(() -> new A(snd.getId(), snd.getName(), new ArrayList<>(snd.getList())));
    

    您还可以将此连接器用作通用方法,并使用消费者创建一个从左到右的 reducer,该消费者允许连接使用初始化函数创建的新可变对象。

    public class Reducer {
        public static <A> Collector<A, ?, A> reduce(Function<A, A> initializer, 
                                                    BiConsumer<A, A> combiner) {
            return Collectors.reducing(null, (fst, snd) -> Optional.ofNullable(fst)
                .map(cur -> { combiner.accept(cur, snd); return cur; })
                .orElseGet(() -> initializer.apply(snd)));
        }
        public static <A> Collector<A, ?, A> reduce(Supplier<A> supplier, 
                                                    BiConsumer<A, A> combiner) {
            return reduce((ign) -> supplier.get(), combiner);
        }
    }
    

    并像使用它

    Collection<A> result = list.stream()
        .collect(Collectors.groupingBy(A::getId, Reducer.reduce(
            (cur) -> new A(cur.getId(), cur.getName(), new ArrayList<>(cur.getList())),
            (fst, snd) -> fst.getList().addAll(snd.getList())
        ))).values();
    

    或者如果你有一个初始化集合的空构造函数

    Collection<A> result = list.stream()
        .collect(Collectors.groupingBy(A::getId, Reducer.reduce(A::new,
            (fst, snd) -> {
                fst.getList().addAll(snd.getList());
                fst.setId(snd.getId());
                fst.setName(snd.getName());
            }
        ))).values();
    

    最后,如果您已经拥有其他答案中提到的复制构造函数或合并方法,则可以进一步简化代码或使用Collectors.toMap 方法。

    【讨论】:

      【解决方案3】:

      如果您可以在这里使用 vanilla Java,这是一个非常简单的解决方案。该列表被一次迭代。

      Map<Integer, A> m = new HashMap<>();
      for (A a : list) {
          if (m.containsKey(a.getId()))
              m.get(a.getId()).getList().addAll(a.getList());
          else
               m.put(a.getId(), new A(a.getId(), a.getName(), a.getList()));
      }
      List<A> output =  new ArrayList<>(m.values());
      

      【讨论】:

      • @Jose 你为什么这么认为
      • 一个不错的谢谢。然而,香草 Java 对我来说太甜了。我需要收藏家的苦涩:)
      【解决方案4】:
      Collection<A> merge(List<A> list) {
          return list.stream()
                  .collect(Collectors.toMap(a -> a.id, Function.identity(), this::merge))
                  .values();
      }
      
      A merge(A a1, A a2) {
          if (!a1.name.equals(a2.name)) {
              throw new IllegalArgumentException("We assumed same id means same name");
          }
          return new A(a1.id, a1.name, union(a1.list, a2.list));
      }
      
      List<B> union(List<B> l1, List<B> l2) {
          List<B> result = new ArrayList<>(l1);
          result.addAll(l2);
          return result;
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-11-13
        • 1970-01-01
        • 1970-01-01
        • 2020-06-10
        相关资源
        最近更新 更多