Stream按属性分组取最大值的最优实现及通用Collector提取
嘿,你的问题很有代表性,我来帮你梳理下现有方案的优劣,再搞定那个通用Collector方法的需求~
一、现有方案的优化建议
先说说你目前的两种实现:
- 第一种groupingBy+toList+max:这种方式会先把每个
type的所有Match都收集到List里,再遍历List找最大值。缺点是没必要存储所有元素,尤其是数据量大的时候,内存占用会更高,效率不如直接在收集过程中维护最大值。 - 第二种groupingBy+maxBy:这个比第一种好,因为
maxBy是在流式处理时就实时跟踪当前最大值,不用存全量数据。但美中不足的是maxBy返回的是Optional<Match>,后续还要处理空值判断(虽然你的场景里每个type至少有一个元素,ifPresent不会空,但代码略显繁琐)。
给你推荐更简洁高效的方案——用Collectors.toMap,直接在收集阶段完成重复key的合并逻辑:
Stream.of(new Match("A", 1), new Match("A", 2), new Match("A", 4), new Match("A", 10), new Match("B", 3), new Match("B", 6), new Match("B", 12), new Match("C", 1)) .collect(Collectors.toMap( Match::getType, // 分组key:type Function.identity(), // value:Match实例本身 // 合并规则:当key重复时,保留score更大的那个 (existingMatch, newMatch) -> existingMatch.getScore() >= newMatch.getScore() ? existingMatch : newMatch )) .values() // 直接拿到每个type对应最高score的Match集合 .forEach(m -> System.out.println(m.getType() + ": " + m.getScore()));
这个方案的优势:
- 不需要嵌套
collectingAndThen,代码更直观 - 直接返回
Match实例而非Optional,避免后续空值处理 - 内存效率更高,因为不会存储同一type的所有元素,只保留当前最大值
二、提取通用的distinctMaxByProperty方法
要实现你想要的通用静态方法,我们可以用泛型来适配不同的实体类、分组键和排序属性。这里提供两个重载版本,满足不同场景:
版本1:基于可比较的属性值(比如你的score是int,实现了Comparable)
import java.util.ArrayList; import java.util.List; import java.util.function.Function; import java.util.stream.Collector; import java.util.stream.Collectors; public class CollectorUtils { /** * 按指定分组键分组,保留每组中指定属性值最大的元素 * @param groupingKeyExtractor 提取分组键的函数 * @param valueExtractor 提取用于比较的属性值的函数(属性需实现Comparable) * @return 收集到的结果List */ public static <T, K, V extends Comparable<? super V>> Collector<T, ?, List<T>> distinctMaxByProperty( Function<? super T, K> groupingKeyExtractor, Function<? super T, V> valueExtractor) { return Collectors.collectingAndThen( Collectors.toMap( groupingKeyExtractor, Function.identity(), // 比较属性值,保留较大的那个元素 (a, b) -> valueExtractor.apply(a).compareTo(valueExtractor.apply(b)) >= 0 ? a : b ), map -> new ArrayList<>(map.values()) ); } }
使用方式完全符合你的预期:
Stream.of(new Match("A", 1), new Match("A", 2), new Match("A", 4), new Match("A", 10), new Match("B", 3), new Match("B", 6), new Match("B", 12), new Match("C", 1)) .collect(CollectorUtils.distinctMaxByProperty(Match::getType, Match::getScore)) .forEach(m -> System.out.println(m.getType() + ": " + m.getScore()));
版本2:支持自定义Comparator(更灵活,比如处理null值或复杂比较逻辑)
如果你的比较逻辑不是简单的属性大小,或者属性可能为null,可以用这个版本:
import java.util.ArrayList; import java.util.List; import java.util.Comparator; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.stream.Collector; import java.util.stream.Collectors; public class CollectorUtils { public static <T, K> Collector<T, ?, List<T>> distinctMaxByProperty( Function<? super T, K> groupingKeyExtractor, Comparator<? super T> comparator) { return Collectors.collectingAndThen( Collectors.toMap( groupingKeyExtractor, Function.identity(), // 用自定义Comparator找最大值 BinaryOperator.maxBy(comparator) ), map -> new ArrayList<>(map.values()) ); } }
使用时传入Comparator:
.collect(CollectorUtils.distinctMaxByProperty( Match::getType, Comparator.comparing(Match::getScore) ))
总结
- 日常场景下,
toMap的方案比groupingBy+maxBy更简洁高效 - 通用方法可以大大提升代码复用性,尤其是在多个地方需要类似分组取最大值的逻辑时
内容的提问来源于stack exchange,提问作者JDC
相关产品推荐
相关产品推荐

