Grouping and summing using BigDecimal parallelly in java 8

I have a list of products with amount one of the attributes. And the list can contain common product name with other attributes being different. So i want to group the list by product and the sum of the amount of the products which share common name in java 8 using grouping and Summing.

Example:
[  
   {  
      "name":"Product A",
      "amount":"40.00",
      "description":"Info1",
      "number":"65"
   },
   {  
      "name":"Product A",
      "amount":"50.00",
      "description":"Info2",
      "number":"67"
   },
   {  
      "name":"Product A",
      "amount":"100.00",
      "description":"Info3",
      "number":"87"
   },
   {  
      "name":"Product B",
      "amount":"45.00",
      "description":"Info4",
      "number":"86"
   },
   {  
      "name":"Product D",
      "amount":"459.00",
      "description":"Info5",
      "number":"7"
   },
   {  
      "name":"Product B",
      "amount":"50.00",
      "description":"Info6",
      "number":"8"
   }
]

The output should be similar to this:

{  
   "Product A = 190.00":[  
      {  
         "name":"Product A",
         "amount":"40.00",
         "description":"Info1",
         "number":"65"
      },
      {  
         "name":"Product A",
         "amount":"50.00",
         "description":"Info2",
         "number":"67"
      },
      {  
         "name":"Product A",
         "amount":"100.00",
         "description":"Info3",
         "number":"87"
      }
   ],
   "Product B=95.00":[  
      {  
         "name":"Product B",
         "amount":"45.00",
         "description":"Info4",
         "number":"86"
      },
      {  
         "name":"Product B",
         "amount":"50.00",
         "description":"Info6",
         "number":"8"
      }
   ],
   "Product D = 459.00":[  
      {  
         "name":"Product D",
         "amount":"459.00",
         "description":"Info5",
         "number":"7"
      }
   ]

I have created a bean class ProductBean which have all the fields (name, amount, description and number) and getters and setters for the same. And productBeans has list of all the products.

Map<String, List<ProductBean>> groupByProduct = 
               productBeans.stream()
                     .collect(Collectors.groupingBy(item -> item.getName()))

Map<String, BigDecimal> result = 
      productBeans.stream()
             .collect(Collectors.groupingBy(ProductBean::getName, 
                                Collectors.mapping(ProductBean::getAmount, 
                     Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));

groupByProduct has list of products grouped with name. result gives the map of product as the key and total amount of that product as value.

But here i'm trying to map the product and total amount to the list of products. I tried to combine the above code to get the expected output but not able to achieve. And same has been achieved by iterating through the map.

But any help in merging the above code to get the map in which product name and amount as key and list as value would be helpful.

3 answers

  • answered 2018-04-14 17:22 lexicore

    Well, you almost got it.

    Collectors.groupingBy allows a second parameter, which is a collector that will produce value for the key. You only need to pass your collector from the second line as this second parameter:

        Map<String, BigDecimal> result = productBeans
                .stream()
                .collect(
                    Collectors.groupingBy(ProductBean::getName,
                    Collectors.mapping(ProductBean::getAmount, 
                        Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));
    

  • answered 2018-04-14 17:57 Aominè

    It seems that you want something as such of Map<String, BigDecimal, List<ProductBean>> where the String represents the name, the BigDecimal being the summation of all the Product's amount and List<ProductBean> being the list of products in that specific group.

    such a map is not possible, so instead, you've managed to do it in two steps which is not very efficient and it doesn't keep related data together rather it separates it into two maps.

    My suggestion is to create a wrapper class as such:

    class ResultSet {
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public BigDecimal getAmount() {
            return amount;
        }
    
        public void setAmount(BigDecimal amount) {
            this.amount = amount;
        }
    
        public List<ProductBean> getProductBeans() {
            return productBeans;
        }
    
        public void setProductBeans(List<ProductBean> productBeans) {
            this.productBeans = productBeans;
        }
    
        private String name;
        private BigDecimal amount;
        private List<ProductBean> productBeans;
    
        @Override
        public String toString() {
            return "ResultSet{" +
                    "name='" + name + '\'' +
                    ", amount=" + amount +
                    ", productBeans=" + productBeans +
                    '}';
        }
    }
    

    Which will wrap a given group of ProductBean's maintaining related data both for better maintainability and arguably readability.

    Now, you can accomplish the task at hand with:

    List<ResultSet> result = productList.stream()
                    .collect(Collectors.groupingBy(ProductBean::getName))
                    .entrySet()
                    .stream()
                    .map(e -> {
                        ResultSet resultSet = new ResultSet();
                        BigDecimal sum = e.getValue().stream()
                                .map(ProductBean::getAmount)
                                .reduce(BigDecimal.ZERO, BigDecimal::add);
                        resultSet.setName(e.getKey());
                        resultSet.setAmount(sum);
                        resultSet.setProductBeans(e.getValue());
                        return resultSet;
                    }).collect(Collectors.toList());
    

    This groups by the ProductBean name, and then maps each group, to a ResultSet instance which will encapsulate the name, summation of the amount in that specific group and the entire ProductBean's in the group.

    You can further refactor the above stream query by creating a method for the mapping:

    private static ResultSet mapToResultSet(Map.Entry<String, List<ProductBean>> e) {
            ResultSet resultSet = new ResultSet();
            BigDecimal sum = e.getValue().stream()
                    .map(ProductBean::getAmount)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            resultSet.setName(e.getKey());
            resultSet.setAmount(sum);
            resultSet.setProductBeans(e.getValue());
            return resultSet;
    }
    

    Thus, rendering the stream query to:

    List<ResultSet> result = productList.stream()
                    .collect(Collectors.groupingBy(ProductBean::getName))
                    .entrySet()
                    .stream()
                    .map(Main::mapToResultSet) // Main representing the class containing the mapToResultSet method
                    .collect(Collectors.toList());
    

  • answered 2018-04-15 11:44 Aominè

    In addition to my previous answer which recommends creating a wrapper class. if for any reason one doesn't want to create a custom class solely to wrap the results, then another solution is as follows:

    List<AbstractMap.SimpleEntry<Map.Entry<String, List<ProductBean>>, BigDecimal>> 
             resultSet =
                    productBeans.stream()
                                .collect(Collectors.groupingBy(ProductBean::getName))
                                .entrySet()
                                .stream()
                                .map(Main::mapToSimpleEntry)
                                .collect(Collectors.toList());
    

    where Main is the class containing mapToSimpleEntry and mapToSimpleEntry is defined as:

    private static AbstractMap.SimpleEntry<Map.Entry<String, List<ProductBean>>, BigDecimal> mapToSimpleEntry(Map.Entry<String, List<ProductBean>> e) {
            BigDecimal sum =
                    e.getValue().stream()
                            .map(ProductBean::getAmount)
                            .reduce(BigDecimal.ZERO, BigDecimal::add);
            return new AbstractMap.SimpleEntry<>(e, sum);
    }
    

    This solution has the advantage that:

    1. one doesn't need to create a custom class
    2. less code