Search code examples
javajava-streamcollectorsreducing

Correct syntax for Collectors.reducing


I have an Order class and a LineItem class like below:

@AllArgsConstructor
@Getter
@ToString
static class Order {
    long orderId;
    List<LineItem> lineItems;
}

@AllArgsConstructor
@Getter
@ToString
static class LineItem {
    String name;
    BigDecimal price;
}

and a list of orders, from which I want to get a map Map<String,BigDecimal> totalByItem where key is name of LineItem and value total price from all orders in the list. For this I want to use Collectors.groupingBy in combination with Collectors.reducing but struggling with the correct syntax. Can someone help?

List<Order> orders = List.of(new Order(1L, List.of(new LineItem("Item-A", BigDecimal.valueOf(1)),
                                                   new LineItem("Item-B", BigDecimal.valueOf(2)),
                                                   new LineItem("Item-C", BigDecimal.valueOf(3)))),
                             new Order(2L, List.of(new LineItem("Item-A", BigDecimal.valueOf(1)),
                                                   new LineItem("Item-D", BigDecimal.valueOf(4)),
                                                   new LineItem("Item-E", BigDecimal.valueOf(5)))),
                             new Order(3L, List.of(new LineItem("Item-B", BigDecimal.valueOf(2)),
                                                   new LineItem("Item-C", BigDecimal.valueOf(3)),
                                                   new LineItem("Item-D", BigDecimal.valueOf(4)))));

what to put where there are ??? now?

Map<String,BigDecimal> totalByItem =
         orders.stream()
                 .flatMap(order -> order.getLineItems().stream())
                 .collect(Collectors.groupingBy(LineItem::getName,
                          lineItem -> Collectors.reducing(BigDecimal.ZERO,(a,b) -> ???)));

Solution

  • groupingBy takes a Collector as its second argument, so you should not pass the lambda lineItem -> ..., and instead pass the Collector.reducing(...) directly.

    Also, since you are reducing a bunch of LineItems to one BigDecimal, you should use the three-parameter overload of reducing, with a mapper

    public static <T, U> Collector<T,?,U> reducing(
        U identity,
        Function<? super T,? extends U> mapper,
        BinaryOperator<U> op)
    

    The mapper is where you specify how a LineItem into a BigDecimal. You probably confused this with the second parameter of groupingBy.

    So to summarise:

    Map<String,BigDecimal> totalByItem =
        orders.stream()
            .flatMap(order -> order.getLineItems().stream())
            .collect(
                Collectors.groupingBy(
                    LineItem::getName,
                    Collectors.reducing(
                        BigDecimal.ZERO,
                        LineItem::getPrice, // <----
                        BigDecimal::add
                    )
                )
            );
    

    As Holger commented, the entire groupingBy collector can also be replaced with a toMap collector, without using reducing at all.

    .collect(
        Collectors.toMap(
            LineItem::getName, // key of the map
            LineItem::getPrice, // value of the map
            BigDecimal::add // what to do with the values when the keys duplicate
        )
    );