
本文介绍如何在 java stream 中通过可变字段组合(如 name+price 或仅 ref1)对对象列表进行去重,避免硬编码、反射和字符串拼接风险,提供类型安全、线程安全且可复用的 distinctbykey 实现方案。
本文介绍如何在 java stream 中通过可变字段组合(如 name+price 或仅 ref1)对对象列表进行去重,避免硬编码、反射和字符串拼接风险,提供类型安全、线程安全且可复用的 distinctbykey 实现方案。
在实际开发中,我们常需根据运行时决定的字段组合对集合进行去重(例如:按 name 和 price 联合去重,或仅按 ref1 去重),而非固定字段。此时若依赖字符串解析(如 "p.getName() + p.getPrice()")不仅无法编译,还丧失类型检查与 IDE 支持,极易引发运行时错误。
正确的做法是:将字段提取逻辑抽象为 Function<pojo></pojo>,由调用方显式传入组合键生成器。这既保持类型安全,又完全解耦去重逻辑与业务键定义。
以下是优化后的完整实现:
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
public class DistinctByKeyExample {
// 线程安全、泛型安全的 distinctByKey 工具方法
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
// 示例 POJO 类(使用 record 更简洁,此处保留原始结构)
static class Pojo {
final String name;
final Integer price;
final String ref1;
Pojo(String name, Integer price, String ref1) {
this.name = name;
this.price = price;
this.ref1 = ref1;
}
String getName() { return name; }
Integer getPrice() { return price; }
String getRef1() { return ref1; }
@Override
public String toString() {
return "Pojo(name=" + name + ", price=" + price + ", ref1=" + ref1 + ")";
}
}
public static void main(String[] args) {
List<Pojo> data = Arrays.asList(
new Pojo("123", 100, "A"),
new Pojo("123", 100, "B"),
new Pojo("123", 100, "C"),
new Pojo("456", 200, "D")
);
// ✅ 按 name + price 联合去重(推荐使用 List 作为复合键,避免字符串拼接歧义)
List<Pojo> byNameAndPrice = data.stream()
.filter(distinctByKey(p -> List.of(p.getName(), p.getPrice())))
.collect(Collectors.toList());
System.out.println("By name + price: " + byNameAndPrice);
// 输出:[Pojo(name=123, price=100, ref1=A), Pojo(name=456, price=200, ref1=D)]
// ✅ 仅按 ref1 去重
List<Pojo> byRef1 = data.stream()
.filter(distinctByKey(Pojo::getRef1))
.collect(Collectors.toList());
System.out.println("By ref1: " + byRef1);
// 输出:[Pojo(name=123, price=100, ref1=A), Pojo(name=123, price=100, ref1=B), ...](全部保留,因 ref1 各不相同)
// ✅ 按 name 单字段去重
List<Pojo> byName = data.stream()
.filter(distinctByKey(Pojo::getName))
.collect(Collectors.toList());
System.out.println("By name: " + byName);
// 输出:[Pojo(name=123, price=100, ref1=A), Pojo(name=456, price=200, ref1=D)]
}
}关键设计说明:
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
立即学习“Java免费学习笔记(深入)”;
- 使用
ConcurrentHashMap.newKeySet()替代Map<object boolean></object>,语义更清晰(add()返回true表示首次插入),且避免putIfAbsent(..., true)的冗余布尔值存储; - 复合键推荐使用
List.of(f1, f2, ...)而非字符串拼接(如"f1_f2"),规避分隔符冲突(如name="AB", price=1vsname="A", price="B1")及空值处理问题; -
distinctByKey是纯函数式工具,与具体类解耦,可复用于任意类型(distinctByKey(User::getEmail)、distinctByKey(Order::getOrderId)); - 所有键提取逻辑在编译期校验,IDE 可自动补全、静态检查,杜绝运行时
NoSuchMethodException。
注意事项:
- 若字段可能为
null,建议使用Objects.equals()安全比较,或提前Optional.ofNullable(...).orElse(placeholder)处理; -
ConcurrentHashMap适用于并行流(.parallelStream()),但若确定单线程执行,也可用HashSet提升性能; - 不要将
distinctByKey误用于需要稳定排序的场景——它保留首次出现元素,但不改变原始顺序(符合 Streamfilter语义)。
通过该方案,你只需一行 lambda 表达式即可灵活切换去重维度,真正实现“变量参数”驱动的流式去重。

















