Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8 stream groupBy pojo

I have a collection of pojos:

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

I need to iterate over collection, groupBy Foos by name and sum counts, then create new collection with pojos with summed count.

Here is how I do it now:

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

Is there a more beauty way to do it with streams?

UPDATE Thanks everyone for help. All answers were great. I decided to create merge function in pojo.

The final solution looks like:

Collection<Foo> groupedFoos = foosToSum.stream()
                    .collect(Collectors.toMap(Foo::getName, Function.identity(), Foo::merge))
                    .values();
like image 576
Kirill Bazarov Avatar asked Apr 13 '18 09:04

Kirill Bazarov


2 Answers

You can do it either using groupingBy or using toMap collector, as for which to use is debatable so I'll let you decide on the one you prefer.

For better readability, I'd create a merge function in Foo and hide all the merging logic inside there.

This also means better maintainability as the more complex the merging gets, you only have to change one place and that is the merge method, not the stream query.

e.g.

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

Now you can do:

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

Note, the above merge function mutates the objects in the source collection, if instead, you want to keep it immutable then you can construct new Foo's like this:

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

Further, if for some reason you explicitly require a List<Foo> instead of Collection<Foo> then it can be done by using the ArrayList copy constructor.

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

Update

As @Federico has mentioned in the comments the last merge function above is expensive as it creates unnecessary objects that could be avoided. So, as he has suggested, a more friendly alternative is to proceed with the first merge function I've shown above and then change your stream query to this:

Collection<Foo> resultSet = foosToSum.stream()
                .collect(Collectors.toMap(Foo::getName,
                        f -> new Foo(f.getName(), null, f.getCount()), Foo::merge))
                .values();
like image 76
Ousmane D. Avatar answered Sep 17 '22 21:09

Ousmane D.


Yes, you could use a downstream collector in your groupingBy to immediately sum the counts. Afterwards, stream the map and map to 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());

A more efficient solution could avoid grouping into a map only to stream it immediately, but sacrifices some readability (in my opinion):

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

By reducing Foos instead of ints, we keep the name in mind and can immediately sum into Foo.

like image 25
Malte Hartwig Avatar answered Sep 20 '22 21:09

Malte Hartwig