Java stream groupingBy and sum multiple fields

一曲冷凌霜 提交于 2021-01-27 18:54:39

问题


Here is my List fooList

class Foo {
    private String name;
    private int code;
    private int account;
    private int time;
    private String others;

    ... constructor, getters & setters
}

e.g.(all the value of account has been set to 1)

new Foo(First, 200, 1, 400, other1), 
new Foo(First, 200, 1, 300, other1),
new Foo(First, 201, 1, 10, other1),
new Foo(Second, 400, 1, 20, other2),
new Foo(Second, 400, 1, 40, other2),
new Foo(Third, 100, 1, 200, other3),
new Foo(Third, 101, 1, 900, other3)

I want to transform these values by grouping "name" and "code", accounting the number, and summing the "time", e.g.

new Foo(First, 200, 2, 700, other1), 
new Foo(First, 201, 1, 10, other1),
new Foo(Second, 400, 2, 60, other2),
new Foo(Third, 100, 1, 200, other3),
new Foo(Third, 101, 1, 900, other3)

I know that I should use stream like this:

Map<String, List<Foo>> map = fooList.stream().collect(groupingBy(Foo::getName()));

but how can I group them by code then do the accounting and summing job ?


Also, what if I want to calculate the average time? e.g.

new Foo(First, 200, 2, 350, other1), 
new Foo(First, 201, 1, 10, other1),
new Foo(Second, 400, 2, 30, other2),
new Foo(Third, 100, 1, 200, other3),
new Foo(Third, 101, 1, 900, other3)

can I use both of summingInt(Foo::getAccount) and averagingInt(Foo::getTime) instead ?


回答1:


A workaround could be to deal with grouping with key as List and casting while mapping back to object type.

List<Foo> result = fooList.stream()
        .collect(Collectors.groupingBy(foo ->
                        Arrays.asList(foo.getName(), foo.getCode(), foo.getAccount()),
                Collectors.summingInt(Foo::getTime)))
        .entrySet().stream()
        .map(entry -> new Foo((String) entry.getKey().get(0),
                (Integer) entry.getKey().get(1),
                entry.getValue(),
                (Integer) entry.getKey().get(2)))
        .collect(Collectors.toList());

Cleaner way would be to expose APIs for merge function and performing a toMap.


Edit: The simplification with toMap would look like the following

List<Foo> result = new ArrayList<>(fooList.stream()
        .collect(Collectors.toMap(foo -> Arrays.asList(foo.getName(), foo.getCode()),
                Function.identity(), Foo::aggregateTime))
        .values());

where the aggregateTime is a static method within Foo such as this :

static Foo aggregateTime(Foo initial, Foo incoming) {
    return new Foo(incoming.getName(), incoming.getCode(),
            incoming.getAccount(), initial.getTime() + incoming.getTime());
}



回答2:


You can implement your own collect mechanish that might look like following

        var collect = Stream.of(
                new Foo(1, 200, 1, 400),
                new Foo(1, 200, 1, 300),
                new Foo(1, 201, 1, 10),
                new Foo(2, 400, 1, 20),
                new Foo(2, 400, 1, 40),
                new Foo(3, 100, 1, 200),
                new Foo(3, 101, 1, 900)
        )
                .collect(
                        ArrayList::new,
                        (BiConsumer<ArrayList<Foo>, Foo>) (foos, foo) -> {
                            var newFoo = foos
                                    .stream()
                                    .filter(f -> f.name == foo.name && f.account == foo.account)
                                    .findFirst()
                                    .map(f -> new Foo(f.name, f.code, f.account + foo.account, f.time + foo.time))
                                    .orElse(foo);
                            foos.removeIf(f -> f.name == foo.name && f.account == foo.account);
                            foos.add(newFoo);
                        },
                        (foos, foos2) -> foos.addAll(foos2)
                );


来源:https://stackoverflow.com/questions/62286182/java-stream-groupingby-and-sum-multiple-fields

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!