Search code examples
javajava-8java-streamaggregationcomposition

Mapping, aggregating and composing totals using Java 8 Streams


I'm trying to recreate a process to create a list of objects that are an aggregation of another list of objects using Java 8 Streams.

for example, I have a class, described below, that is provided from a database call or similar

public class Order {

    private String orderNumber;        
    private String customerNumber;
    private String customerGroup;
    private Date deliveryDate;
    private double orderValue;
    private double orderQty;
}

Elsewhere in my application I have a class OrderTotal which represents and aggregation of Order grouping by customer number and group and summing the totals of orderValue and orderQty. (With an equals and hashcode on customerGroup and customerNumber)

public class OrderTotal {

    private String customerGroup;
    private String customerNumber;
    private double totalValue;
    private double totalQty;
}

The 'long hand' way we have achieved this prior to java 8 is as follows

public Collection<OrderTotal> getTotals(List<Order> orders) {
    ///map created for quick access to the order total for each order 
    Map<OrderTotal, OrderTotal> map = new HashMap<>();
    ///loop through all orders adding to the relevaent order total per iteration
    for (Order order : orders) {
        OrderTotal orderTotal = createFromOrder(order);
        {
            ///if the order total already exists in the map use that one, otherwise add it to the map.
            OrderTotal temp = map.get(orderTotal);
            if(temp == null){
                map.put(orderTotal, orderTotal);
            }else{
                orderTotal = temp;
            }
        }
        ///add the values to the total 
        aggregate(orderTotal, order);
    }        
    return map.values();
}

private OrderTotal createFromOrder(Order order) {
    OrderTotal orderTotal = new OrderTotal();
    orderTotal.setCustomerGroup(order.getCustomerGroup());
    orderTotal.setCustomerNumber(order.getCustomerNumber());
    return orderTotal;
}

private void aggregate(OrderTotal orderTotal, Order order){
    orderTotal.setTotalQty(orderTotal.getTotalQty() + order.getOrderQty());
    orderTotal.setTotalValue(orderTotal.getTotalValue() + order.getOrderValue());
}

Ive been looking at Collectors using a grouping by and reduction functions but they all seem focused on aggregating the order class rather than composing the totals in the OrderTotal class.

I'm looking for a tidy stream or collect function that removes all the bloat from this code.


Solution

  • You can use the toMap collector as follows:

    Collection<OrderTotal> result = orders.stream()
                .map(o -> createFromOrder(o))
                .collect(toMap(Function.identity(),
                            Function.identity(),
                            (l, r) -> {
                                aggregate(l, r);
                                return l;
                            }))
                    .values();
    

    Note that this requires changing the aggregate method parameters to aggregate(OrderTotal orderTotal, OrderTotal order){ ... } i.e. both parameters are of type OrderTotal.

    or you could remove the aggregate method entirely and perform the logic in the toMap:

    Collection<OrderTotal> result = orders.stream()
                .map(o -> createFromOrder(o))
                .collect(toMap(Function.identity(),
                        Function.identity(),
                        (l, r) -> {
                            l.setTotalQty(l.getTotalQty() + r.getTotalQty());
                            l.setTotalValue(l.getTotalValue() + r.getTotalValue());
                            return l;
                        }))
                .values();