Stream 过滤后修改元素,却意外修改原列表
1. 问题重现
List<StringBuffer> list = Lists.newArrayList(new StringBuffer("a"),new StringBuffer("b")
);
List<StringBuffer> filterList = list.stream().filter(v -> "a".equalsIgnoreCase(v.toString())).collect(Collectors.toList());for (StringBuffer v : filterList) {v.append("b");
}System.out.println(list); // 输出:[ab, b]
看似只是想处理过滤结果集,但是最终打印原 list 却变成 [ab, b]。为什么会这样?
2. 原因分析
这是因为引用传递导致原始数据被修改,Java 对象是通过引用在集合中传递的,Stream.filter 和 collect 并不会自动复制对象,只是把满足条件的对象引用留下来形成新的列表。结果导致filter 后的 filterList 和原 list 指向的是同一个 StringBuffer 实例。
3. 正确做法
3.1 方案一:深拷贝元素再修改
如果你只想处理过滤后的数据,避免污染原列表状态,需要对每个元素执行深拷贝(Clone 或构造新实例)。
List<StringBuffer> safeList = list.stream().filter(v -> "a".equalsIgnoreCase(v.toString())).map(v -> new StringBuffer(v.toString())).collect(Collectors.toList());for (StringBuffer v : safeList) {v.append("b");
}
System.out.println(list); // [a, b]
System.out.println(safeList); // [ab]
3.2 方案二:手动 new 集合再操作
List<StringBuffer> safeList = new ArrayList<>();
for (StringBuffer sb : list) {if ("a".equalsIgnoreCase(sb.toString())) {safeList.add(new StringBuffer(sb.toString()));}
}
3.3 方案三:如果只是想批量修改原列表元素
如果业务是要修改原列表的对象状态,用 forEach 或 replaceAll 更清晰。
// forEach
list.replaceAll(sb -> {if ("a".equalsIgnoreCase(sb.toString())) {sb.append("b");}return sb;
});// replaceAll
list.stream().filter(v -> "a".equalsIgnoreCase(v.toString())).forEach(v -> v.append("b"));
4. 补充
Stream 的行为应尽量无副作用(no side-effects),peek 操作主要用于调试而非修改状态 。
虽然技术上可以用 .peek(u -> u.setXxx(…)) 修改对象,但这违反函数式编程设计原则,应尽量避免。