Java Streams - group by two criteria summing result

Is there any way to solve the task above in one stream?

It depends on what you mean by "in one stream". You want to perform a reduction operation that is probably best characterized as a composite of a sequence of reductions:

  • group the orders by month
  • within each monthly group, aggregate the orders for each customer to yield a total amount
  • among each monthly group of per-customer aggregate results, choose the one with the greatest amount (note: not well defined in the case of ties)

From the perspective of the Stream API, performing any one of those individual reductions on a stream is a terminal operation on that stream. You can process the result with a new stream, even chaining that together syntactically, but although that might take the syntactic form of a single chain of method invocations, it would not constitute all operations happening on a single stream.

You could also create a single Collector (or the components of one) so that you get the result directly by collecting the stream of your input elements, but internally, that collector would still need to perform the individual reductions, either by internally creating and consuming additional streams, or by performing the same tasks via non-stream APIs. If you count those internal operations then again, no, it would not constitute performing operations on a single stream. (But if you don't consider those internal reductions, then yes, this does it all on one stream.)


Try using groupingBy, summingLong and comparingLong like as shown below

Map<Month, BuyerDetails> topBuyers = orders.stream()
    .collect(Collectors.groupingBy(Order::getOrderMonth,
             Collectors.groupingBy(Order::getCustomer,
             Collectors.summingLong(Order::getAmount))))
    .entrySet().stream()
    .collect(Collectors.toMap(Map.Entry::getKey,
             order -> order.getValue().entrySet().stream()
            .max(Comparator.comparingLong(Map.Entry::getValue))
            .map(cust -> new BuyerDetails(cust.getKey(), cust.getValue())).get()));

Output

{
  "MARCH": { "customer": "Dan", "amount": 300 }, 
  "APRIL": { "customer": "Jenny", "amount": 550 }
}

It has a nested stream, so it is not one stream and it returns Map<String, Optional<BuyerDetails>>.


orders.stream()
        .collect(
            Collectors.groupingBy(Order::getOrderMonth,
                Collectors.collectingAndThen(
                        Collectors.groupingBy(
                                Order::getCustomer,
                                Collectors.summarizingLong(Order::getAmount)
                        ),
                        e -> e.entrySet()
                                .stream()
                                .map(entry -> new BuyerDetails(entry.getKey(), entry.getValue().getSum()))
                                .max(Comparator.comparingLong(BuyerDetails::getAmount))
                )
            )
        )

so there 3 steps:

  • Group by month Collectors.groupingBy(Order::getOrderMonth,
  • Group by customer name and summing total order amount Collectors.groupingBy(Order::getCustomer, Collectors.summarizingLong( Order::getAmount))
  • filtering intermediate result and leaving only customers with maximum amount max(Comparator.comparingLong(BuyerDetails::getAmount))

output is

{
  APRIL = Optional [ BuyerDetails { customer = 'Jenny', amount = 550 } ],
  MARCH = Optional [ BuyerDetails { customer = 'Dan', amount = 300 } ]
}

I am curious as well if this can be done without additional stream.