I have a List
of Product
s and every product contains a list of Ingredient
s.
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 Product
s 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}]