Java 用 Supplier 返回同一个 Stream,第二次 sorted 抛出 IllegalStateException
Supplier 返回已有 Stream 引用时仍会复用同一条流。独立计算应从数据源分别创建流,连续处理则串成一条链,并保留资源与数据快照边界。
把一个已经创建的 Stream 包装成 Supplier 后,多次调用 get() 仍可能遇到 IllegalStateException: stream has already been operated upon or closed。有时连 collect() 或 count() 都还没调用,第二次 sorted() 就报错。
本文针对 Java 8 及以上的标准 Stream API:Supplier 捕获同一个 Stream 对象,而代码试图从这个对象创建两条处理链。
看 get 返回的是已有对象,还是新建的流
下面的 Supplier 只是返回同一个引用:
Stream<String> shared = Arrays.asList("pear", "apple").stream();
Supplier<Stream<String>> source = () -> shared;
source.get().sorted();
source.get().sorted();
第一次 sorted() 已把原流接入处理链。即使没有终止操作,随后再次从原流分叉也不符合 Stream 的使用约定。标准实现能够检测到这类复用时会抛出异常;不要依靠“还没执行终止操作”判断流能否重用。
排查时直接查看 Supplier 的 lambda 返回了什么。返回已保存的 Stream,不等于每次创建新的 Stream。
两次独立计算:从可重复读取的数据源分别建流
如果确实需要对同一集合做两次计算,把创建 Stream 的调用放进 Supplier:
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StreamExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("pear", "apple");
Supplier<Stream<String>> source = () -> names.stream();
List<String> sorted = source.get()
.sorted()
.collect(Collectors.toList());
long count = source.get().count();
System.out.println(sorted);
System.out.println(count);
}
}
预期结果分别是 [apple, pear] 和 2。每次 get() 都从集合创建新的流,两个终止操作各自消费自己的处理链。示例使用 Java 8 已有的 API。
新建流不代表冻结数据。如果集合在两次计算之间改变,结果也可能不同;如果两次计算必须基于同一份数据,应在应用允许的同步边界内先取得快照,再对快照建流。不要在流遍历期间随意修改普通集合。
只需要一份结果:把中间操作串成一条链
如果第二次操作只是接着处理第一次的结果,直接链式调用即可:
List<String> result = names.stream()
.filter(name -> name.length() > 4)
.sorted()
.collect(Collectors.toList());
对示例集合,预期结果是 [apple]。中间操作返回的流继续参与同一条链,最后调用一次终止操作。不要保存最初的流,然后从它反复调用不同的中间操作。
手里若只有一个已经接入处理链或已经消费的流,没有通用的“倒带”方法。需要回到能重新打开的数据源,或在第一次消费时保存必要结果。缓存大规模或无界数据会带来内存风险,不应为了复用而无条件收集全部内容。
涉及文件等 I/O 数据源时,新建流还意味着重新打开资源,需按该数据源要求关闭每次创建的流;集合流示例不能代替资源管理。
改动后分别验证两次独立计算都能完成、单条处理链结果符合预期。若性能或资源占用发生变化,回退前保留原实现并重新选择一次计算或缓存结果的设计,不能把重复返回同一 Stream 当作兼容方案。


