Home > other >  Creating a Map<U, List<T>> from a List<T> where each T contains a List<U> us
Creating a Map<U, List<T>> from a List<T> where each T contains a List<U> us

Time:08-31

I have a List of Products and every product contains a list of Ingredients.

public class Product {
    private String id;
    private List<Ingredient> ingredients;

    // other code
}

public class Ingredient {
    private String name;

    // other code
}

I want to collect products into Map grouping them by each ingredient.

How can I do it by using Stream API's collect() and Collectors.groupingBy()?

Product p1 = new Product("1", Arrays.asList(new Ingredient("i1"), new Ingredient("i2"), new Ingredient("i3")));
Product p2 = new Product("2", Arrays.asList(new Ingredient("i1"), new Ingredient("i3"), new Ingredient("i5")));
Product p3 = new Product("3", Arrays.asList(new Ingredient("i2"), new Ingredient("i4"), new Ingredient("i5")));

List<Product> products = List.of(p1, p2, p3);
    
Map<Ingredient, List<Product>> result = products.stream()
    .collect(Collectors.groupingBy( not_sure_what_needs_to_go_here ));

The expected result should look like:

[i1 : {p1, p2} ,
 i2 : {p1, p3} ,
 i3 : {p1, p2},
 i4 : {p3} ,
 i5 : {p2, p2}]

CodePudding user response:

You can create a Tuple

@Value
static class Tuple<L, R> {
    L left;
    R right;
}

or a Java record

public record IngProd (Ingredient ingredient, Product product) {}

and then stream over your products flatmapping to a tuple and collect using grouping by

Map<Ingredient, List<Product>> result =
        products.stream()
                .flatMap(prod -> prod.getIngredients().stream().map(ing -> new Tuple<>(ing, prod)))
                .collect(groupingBy(Tuple::getLeft, mapping(Tuple::getRight, toList())));

when using a record change

Tuple::getLeft & Tuple::getRight

to

IngProd::getIngredient & IngProd::getProduct

CodePudding user response:

It can't be achieved using by collector groupingBy() without introducing some kind of intermediary objects that will hand out a separate ingredient to the collector, instead of a collection of ingredients.

But it would be redundant, in the sense that it would cost the performance (object creation doesn't happen for free) and we can do better.

Instead, we can accumulate Products from the stream without transforming them directly into the resulting Map perfectly fine, by making use of the three-args version of collect(supplier,accumulator,combiner).

Note that in order to serve as a key in a Map, Ingredient need to have a proper implementation of the equals/hashCode.

That's how it might be implemented:

List<Product> products = List.of(p1,p2,p3);
        
Map<Ingredient, List<Product>> productsByIngredient = products.stream()
    .collect(
        HashMap::new,
        (Map<Ingredient, List<Product>> map, Product next) -> next.getIngredients()
            .forEach(i -> map.computeIfAbsent(i, k -> new ArrayList<>()).add(next)),
        (left, right) -> right.forEach((i, prods) ->
            left.merge(i, prods, (oldV, newV) -> { oldV.addAll(newV); return oldV; }))
    );
    
productsByIngredient.forEach((i, prods) -> System.out.println(i   " -> "   prods));

Output:

Ingredient{name='i1'} -> [Product{id='1}, Product{id='2}]
Ingredient{name='i2'} -> [Product{id='1}, Product{id='3}]
Ingredient{name='i3'} -> [Product{id='1}, Product{id='2}]
Ingredient{name='i4'} -> [Product{id='3}]
Ingredient{name='i5'} -> [Product{id='2}, Product{id='3}]

A link to Online Demo

  • Related