Search code examples
javahashmapjava-streamcollectorsgroupingby

How to group Objects by property using using collector groupingBy() on top of flatMap() in Java 8


How to create Map<String,List<Product>> of below. Here, String (key of the Map) is the category of a Product.

One product can belong to multiple categories, like in the example below.

I am trying with below code, however not able to get next operation:

products.stream()
    .flatMap(product -> product.getCategories().stream())
    . // how should I progress from here?

Result should be like below:

{electonics=[p1,p3,p4], fashion=[p1,p2,p4], kitchen=[p1,p2,p3], abc1=[p2], xyz1=[p3],pqr1=[p4]}

Product p1 = new Product(123, Arrays.asList("electonics,fashion,kitchen".split(",")));
Product p2 = new Product(123, Arrays.asList("abc1,fashion,kitchen".split(",")));
Product p3 = new Product(123, Arrays.asList("electonics,xyz1,kitchen".split(",")));
Product p4 = new Product(123, Arrays.asList("electonics,fashion,pqr1".split(",")));
List<Product> products = Arrays.asList(p1, p2, p3, p4);
class Product {

    int price;
    List<String> categories;

    public Product(int price) {
        this.price = price;
    }

    public Product(int price, List<String> categories) {
        this.price = price;
        this.categories = categories;
    }

    public int getPrice() {
        return price;
    }

    public List<String> getCategories() {
        return categories;
    }
}

Solution

  • If you want to use collector groupingBy() for some reason, then you can define a wrapper class (with Java 16+ a record would be more handy for that purpose) which would hold a reference to a category and a product to represent every combination category/product which exist in the given list.

    public record ProductCategory(String category, Product product) {}
    

    Pre-Java 16 alternative:

    public class ProductCategory {
        private String category;
        private Product product;
        
        // constructor and getters
    }
    

    And then in the make use of the combination of collectors mapping() and toList() as the downstream collector of groupingBy().

    List<Product> products = // initializing the list of products
            
    Map<String, List<Product>> productsByCategory = products.stream()
        .flatMap(product -> product.getCategories().stream()
            .map(category -> new ProductCategory(category, product)))
        .collect(Collectors.groupingBy(
            ProductCategory::category,                   // ProductCategory::getCategory if you used a class instead of record
            Collectors.mapping(ProductCategory::product, // ProductCategory::getProduct if you used a class instead of record
                Collectors.toList())
        ));
    

    A link to Online-Demo


    But instead of creating intermediate objects and generating nested streams, the more performant option would be to describe the accumulation strategy within the three-args version of collect() (or define a custom collector).

    That's how it might be implemented:

    Map<String, List<Product>> productsByCategory = products.stream()
        .collect(
            HashMap::new,
            (Map<String, List<Product>> map, Product next) -> next.getCategories()
                .forEach(category -> map.computeIfAbsent(category, k -> new ArrayList<>())
                    .add(next)),
            (left, right) -> right.forEach((k, v) -> 
                left.merge(k, v,(oldProd, newProd) -> { oldProd.addAll(newProd); return oldProd; }))
        );
    

    A link to Online-Demo