2

I have a list of Transactions whom I wanted to :

  • First Group by year
  • Then Group by type for every transaction in that year
  • Then convert the Transactions to Result object having sum of all transaction's value in sub groups.

My Code snippets looks like :

Map<Integer, Map<String, Result> res = transactions.stream().collect(Collectors
                            .groupingBy(Transaction::getYear,
                                groupingBy(Transaction::getType),
                                  reducing((a,b)-> new Result("YEAR_TYPE", a.getAmount() + b.getAmount()))
));

Transaction Class :

class Transaction {

    private int year;
    private String type;
    private int value;
}

Result Class :

class Result {

    private String group;
    private int amount;
}

it seems to be not working, what should I do to fix this making sure it works on parallel streams too?

Naman
  • 27,789
  • 26
  • 218
  • 353
Jimq
  • 362
  • 4
  • 17

2 Answers2

2

I would use a custom collector:

Collector<Transaction, Result, Result> resultCollector =
 Collector.of(Result::new, // what is the result of this collector

  (a, b) -> { a.setAmount( a.getAmount() + b.getValue());
              a.setGroup("YEAR_TYPE"); }, // how to accumulate a result from a transaction

  (l, r) -> { l.setAmount(l.getAmount() + r.getAmount()); return l; }); // how to combine two 
                                                                        // result instances 
                                                                        // (used in parallel streams)

then you can use the collector to get the map:

Map<Integer, Map<String, Result>> collect = transactions.parallelStream().collect(
       groupingBy(Transaction::getYear,
               groupingBy(Transaction::getType, resultCollector) ) );
pero_hero
  • 2,881
  • 3
  • 10
  • 24
2

In the context, Collectors.reducing would help you reduce two Transaction objects into a final object of the same type. In your existing code what you could have done to map to Result type was to use Collectors.mapping and then trying to reduce it.

But reducing without an identity provides and Optional wrapped value for a possible absence. Hence your code would have looked like ;

Map<Integer, Map<String, Optional<Result>>> res = transactions.stream()
        .collect(Collectors.groupingBy(Transaction::getYear,
                Collectors.groupingBy(Transaction::getType,
                        Collectors.mapping(t -> new Result("YEAR_TYPE", t.getValue()),
                                Collectors.reducing((a, b) ->
                                        new Result(a.getGroup(), a.getAmount() + b.getAmount()))))));

to thanks to Holger, one can simplify this further

…and instead of Collectors.mapping(func, Collectors.reducing(op)) you can use Collectors.reducing(id, func, op)


Instead of using this and a combination of Collectors.grouping and Collectors.reducing, transform the logic to use Collectors.toMap as:

Map<Integer, Map<String, Result>> result = transactions.stream()
        .collect(Collectors.groupingBy(Transaction::getYear,
                Collectors.toMap(Transaction::getType,
                        t -> new Result("YEAR_TYPE", t.getValue()),
                        (a, b) -> new Result(a.getGroup(), a.getAmount() + b.getAmount()))));

The answer would stand complete with a follow-up read over Java Streams: Replacing groupingBy and reducing by toMap.

Naman
  • 27,789
  • 26
  • 218
  • 353
  • 4
    …and instead of `Collectors.mapping(func, Collectors.reducing(op))` you can use [`Collectors.reducing(id, func, op)`](https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#reducing-U-java.util.function.Function-java.util.function.BinaryOperator-) where the identity value would be `new Result("YEAR_TYPE", 0)`. – Holger Apr 24 '20 at 07:49
  • 1
    @Holger Thank you. Point noted to recollect this in the future. – Naman Apr 24 '20 at 09:01
  • @Naman Thanks for the explanation, Had another doubt, like what needs to be done if after grouping we need to find average and dump the result back to 'Result' Object ? – Jimq Apr 24 '20 at 10:08
  • 1
    @Jimq you can groupBy and then stream over the entrySet as well but would be much clear with an example to understand the exact requirement. Maybe worth another question if you don't find something existing. – Naman Apr 24 '20 at 10:48