如何使用Stream API的collect()与Collectors.groupingBy()实现按Ingredient对Product列表分组
如何用Stream API按Ingredient分组Product列表?
你遇到的核心问题是:每个Product对应多个Ingredient,而Collectors.groupingBy()默认是给每个流元素分配一个分组键,但我们需要把一个Product关联到它的所有Ingredient上。直接用groupingBy(Product::getIngredients)肯定不行,因为这样键会是整个Ingredient列表,而不是单个Ingredient。
解决思路是先通过flatMap把每个Product拆分成多个「Ingredient + Product」的配对,然后再按Ingredient分组。下面是完整的实现代码:
import java.util.AbstractMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; // 你的Product类定义 public class Product { private String id; private List<Ingredient> ingredients; public Product(String id, List<Ingredient> ingredients) { this.id = id; this.ingredients = ingredients; } public List<Ingredient> getIngredients() { return ingredients; } // 重写toString方便查看结果 @Override public String toString() { return "Product{" + "id='" + id + '\'' + '}'; } } // 你的Ingredient类定义(必须重写equals和hashCode) public class Ingredient { private String name; public Ingredient(String name) { this.name = name; } // 必须重写!否则相同name的Ingredient会被当成不同的键 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Ingredient that = (Ingredient) o; return name.equals(that.name); } @Override public int hashCode() { return name.hashCode(); } // 重写toString方便查看结果 @Override public String toString() { return "Ingredient{" + "name='" + name + '\'' + '}'; } } // 测试代码 public class Main { public static void main(String[] args) { Product p1 = new Product("1", List.of(new Ingredient("i1"), new Ingredient("i2"), new Ingredient("i3"))); Product p2 = new Product("2", List.of(new Ingredient("i1"), new Ingredient("i3"), new Ingredient("i5"))); Product p3 = new Product("3", List.of(new Ingredient("i2"), new Ingredient("i4"), new Ingredient("i5"))); List<Product> products = List.of(p1, p2, p3); Map<Ingredient, List<Product>> result = products.stream() .flatMap(product -> product.getIngredients().stream() .map(ingredient -> new AbstractMap.SimpleEntry<>(ingredient, product))) .collect(Collectors.groupingBy( Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toList()) )); // 打印结果验证 result.forEach((ingredient, productList) -> System.out.println(ingredient + " : " + productList)); } }
代码细节解释:
- flatMap阶段:把每个
Product转换成多个Map.Entry<Ingredient, Product>对象,每个Entry代表这个Product和它的一个Ingredient的关联。flatMap会把所有这些Entry合并成一个流,这样每个Ingredient都能和对应的Product一一对应。 - collect阶段:
- 第一个参数
Map.Entry::getKey指定用Ingredient作为分组的键; - 第二个参数是下游收集器
Collectors.mapping(...),用来把每个Entry中的Product提取出来,最终收集成List。
- 第一个参数
关键注意事项:
一定要给Ingredient类重写equals()和hashCode()方法!因为groupingBy默认使用HashMap来存储分组结果,而HashMap是通过这两个方法判断两个键是否相等的。如果不重写,即使两个Ingredient的name相同,也会被当成不同的键,导致分组结果不符合预期。
如果你使用的是Java 16及以上版本,可以用Record来替代AbstractMap.SimpleEntry,让代码更简洁易读:
// 定义一个Record来存储配对 record IngredientProductPair(Ingredient ingredient, Product product) {} // 对应的stream代码 Map<Ingredient, List<Product>> result = products.stream() .flatMap(product -> product.getIngredients().stream() .map(ingredient -> new IngredientProductPair(ingredient, product))) .collect(Collectors.groupingBy( IngredientProductPair::ingredient, Collectors.mapping(IngredientProductPair::product, Collectors.toList()) ));
运行测试代码后,你会得到和预期完全一致的结果:
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'}]
内容的提问来源于stack exchange,提问作者seyda özdemir
相关产品推荐
相关产品推荐

