Java 8 个流组 作者:pojo

Java 8 stream groupBy pojo

我有一个 pojo 集合:

public class Foo {
    String name;
    String date;
    int count;
}

我需要遍历集合,按名称和总计数对 groupBy Foos 进行迭代,然后使用具有总计数的 pojos 创建新集合。

这是我现在的做法:

    List<Foo> foosToSum = ...

    Map<String, List<Foo>> foosGroupedByName = foosToSum.stream()
            .collect(Collectors.groupingBy(Foo::getName));

    List<Foo> groupedFoos = foosGroupedByName.keySet().stream().map(name -> {
        int totalCount = 0;
        String date = "";
        for(Foo foo: foosGroupedByName.get(name)) {
            totalCount += foo.getCount();
            date = foo.getDate() //last is used
        }
        return new Foo(name, date, totalCount);
    }).collect(Collectors.toList());

有没有更漂亮的流处理方式?

更新 感谢大家的帮助。所有答案都很棒。 我决定在 pojo 中创建合并函数。

最终解决方案如下:

Collection<Foo> groupedFoos = foosToSum.stream()
                    .collect(Collectors.toMap(Foo::getName, Function.identity(), Foo::merge))
                    .values();

是的,您可以在 groupingBy 中使用下游收集器来立即对计数求和。之后,流式传输地图并映射到 Foos。

foosToSum.stream()
         .collect(Collectors.groupingBy(Foo::getName,
                                        Collectors.summingInt(Foo::getCount)))
         .entrySet()
         .stream()
         .map(entry -> new Foo(entry.getKey(), null, entry.getValue()))
         .collect(Collectors.toList());

一个更有效的解决方案可以避免分组到地图中只是为了立即流式传输它,但会牺牲一些可读性(在我看来):

foosToSum.stream()
         .collect(Collectors.groupingBy(Foo::getName,
                                        Collectors.reducing(new Foo(),
                                                            (foo1, foo2) -> new Foo(foo1.getName(), null, foo1.getCount() + foo2.getCount()))))
         .values();

通过减少 Foos 而不是 ints,我们记住了这个名字并可以立即求和为 Foo。

您可以使用 groupingBytoMap 收集器来完成,至于使用哪一个是有争议的,所以我会让您决定您喜欢的那个。

为了更好的可读性,我会在 Foo 中创建一个合并函数,并在其中隐藏所有合并逻辑。

这也意味着更好的可维护性,因为合并变得越复杂,您只需更改一个地方,那就是merge方法,而不是流查询.

例如

public Foo merge(Foo another){
     this.count += another.getCount();
     /* further merging if needed...*/
     return this;
}

现在你可以做:

Collection<Foo> resultSet = foosToSum.stream()
            .collect(Collectors.toMap(Foo::getName,
                    Function.identity(), Foo::merge)).values();

注意,上面的合并函数改变了源集合中的对象,如果相反,你想保持它不可变,那么你可以像这样构造新的Foo

public Foo merge(Foo another){
      return new Foo(this.getName(), null, this.getCount() + another.getCount());
}

此外,如果出于某种原因您 明确 需要 List<Foo> 而不是 Collection<Foo> 那么可以使用 ArrayList复制构造函数。

List<Foo> resultList = new ArrayList<>(resultSet);

更新

正如@Federico 在评论中提到的,上面的最后一个合并函数很昂贵,因为它创建了可以避免的不必要的对象。因此,正如他所建议的那样,一个更友好的选择是继续我上面显示的第一个合并功能,然后将您的流查询更改为:

Collection<Foo> resultSet = foosToSum.stream()
                .collect(Collectors.toMap(Foo::getName,
                        f -> new Foo(f.getName(), null, f.getCount()), Foo::merge))
                .values();