Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Java Stream GroupBy and Reduce

I have an Item class which contains a code, quantity and amount fields, and a list of items which may contain many items (with same code). I want to group the items by code and sum up their quantities and amounts.

I was able to achieve half of it using stream’s groupingBy and reduce. The grouping by worked, but the reduce is reducing all of the grouped items into one single item repeated over the different codes (groupingBy key).

Shouldn’t reduce here reduce the list of items for each code from the map? Why is it retuning the same combined item for all.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

Below is a sample code.

import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.Map;

class HelloWorld {
    public static void main(String[] args) {
        List<Item> itemList = Arrays.asList(
            createItem("CODE1", 1, 12),
            createItem("CODE2", 4, 22),
            createItem("CODE3", 5, 50),
            createItem("CODE4", 2, 11),
            createItem("CODE4", 8, 20),
            createItem("CODE2", 1, 42)
        );
        
        Map<String, Item> aggregatedItems = itemList
            .stream()
            .collect(Collectors.groupingBy(
                Item::getCode,
                Collectors.reducing(new Item(), (aggregatedItem, item) -> {
                    int aggregatedQuantity = aggregatedItem.getQuantity();
                    double aggregatedAmount = aggregatedItem.getAmount();
                    
                    aggregatedItem.setQuantity(aggregatedQuantity + item.getQuantity());
                    aggregatedItem.setAmount(aggregatedAmount + item.getAmount());
                    
                    return aggregatedItem;
                })
            ));
        
        System.out.println("Map total size: " + aggregatedItems.size()); // expected 4
        System.out.println();
        aggregatedItems.forEach((key, value) -> {
            System.out.println("key: " + key);
            System.out.println("value - quantity: " + value.getQuantity() + " - amount: " + value.getAmount());
            System.out.println();
        });
    }
    
    private static Item createItem(String code, int quantity, double amount) {
        Item item = new Item();
        item.setCode(code);
        item.setQuantity(quantity);
        item.setAmount(amount);
        return item;
    }
}

class Item {
    private String code;
    private int quantity;
    private double amount;
    
    public Item() {
        quantity = 0;
        amount = 0.0;
    }
    
    public String getCode() { return code; }
    public int getQuantity() { return quantity; }
    public double getAmount() { return amount; }
    
    public void setCode(String code) { this.code = code; }
    public void setQuantity(int quantity) { this.quantity = quantity; }
    public void setAmount(double amount) { this.amount = amount; }
}

and below is the output.

Map total size: 4

key: CODE2
value - quantity: 21 - amount: 157.0

key: CODE1
value - quantity: 21 - amount: 157.0

key: CODE4
value - quantity: 21 - amount: 157.0

key: CODE3
value - quantity: 21 - amount: 157.0

>Solution :

You must not modify the input arguments to Collectors.reducing. new Item() is only executed once and all your reduction operations will share the same "aggregation instance". In other words: the map will contain the same value instance 4 times (you can easily check yourself with System.identityHashCode() or by comparing for reference-equality: aggregatedItems.get("CODE1") == aggregatedItems.get("CODE2")).

Instead, return a new result instance:

        final Map<String, Item> aggregatedItems = itemList
            .stream()
            .collect(Collectors.groupingBy(
                Item::getCode,
                Collectors.reducing(new Item(), (item1, item2) -> {
                    final Item reduced = new Item();
                    reduced.setQuantity(item1.getQuantity() + item2.getQuantity());
                    reduced.setAmount(item1.getAmount() + item2.getAmount());
                    return reduced;
                })
            ));

Output:

Map total size: 4

key: CODE2
value - quantity: 5 - amount: 64.0

key: CODE1
value - quantity: 1 - amount: 12.0

key: CODE4
value - quantity: 10 - amount: 31.0

key: CODE3
value - quantity: 5 - amount: 50.0
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading