CodeGym /课程 /JAVA 25 SELF /流式处理常见错误解析

流式处理常见错误解析

JAVA 25 SELF
第 32 级 , 课程 4
可用

1. 错误:重复使用流

在 Java 中,Stream 是一次性工具。一旦执行了终止操作(例如 collect()forEach()count()),该流就会关闭,无法继续使用。

错误示例

Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println); // 一切正常
stream.forEach(System.out::println); // 将抛出 IllegalStateException!

输出:

a
b
c
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed

如何避免

如果需要执行多次操作——每次都新建一个流:

List<String> list = List.of("a", "b", "c");
list.stream().forEach(System.out::println); // OK
list.stream().count(); // OK

小技巧: 流就像食堂的一次性纸杯:喝完就扔,下一杯再拿一个新的。

2. 错误:在流遍历期间修改集合

如果在通过流遍历时修改源集合,会得到 ConcurrentModificationException。这在过滤和删除元素时很常见。

错误示例

List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
names.stream().forEach(name -> {
    if (name.startsWith("A")) {
        names.remove(name); // 砰!
    }
});

结果:

Exception in thread "main" java.util.ConcurrentModificationException

正确做法

请使用不会在遍历期间修改集合的方法,或者将结果收集到新集合中:

names.removeIf(name -> name.startsWith("A")); // OK,removeIf 是安全的

// 或使用 Stream API:
List<String> filtered = names.stream()
    .filter(name -> !name.startsWith("A"))
    .collect(Collectors.toList());

请记住: 流讲究“纯净”——当流正在“处理”它时,不要去碰源集合。

3. 错误:对无限流忘记调用 limit

方法 Stream.iterateStream.generate 可能创建无限流。如果忘记用 limit 限制大小,可能会一直卡住(或直到抛出 OutOfMemoryError)。

错误示例

Stream<Integer> infinite = Stream.iterate(0, n -> n + 1);
infinite.forEach(System.out::println); // 程序将永远不会结束!

正确做法

使用 limit 限制流:

Stream<Integer> finite = Stream.iterate(0, n -> n + 1).limit(10);
finite.forEach(System.out::println); // 将输出 0 到 9

提示: 无限流像没有大结局的电视剧——总得有人喊停,最好是你自己!

4. 错误:对 null 处理不当

Stream API 不喜欢 null。如果集合里有 null,很多操作可能会抛出 NullPointerException,尤其是在元素上调用方法时。

错误示例

List<String> names = Arrays.asList("Alice", null, "Bob");
names.stream()
    .map(String::toUpperCase) // 砰!发生在 null 上
    .forEach(System.out::println);

结果:

Exception in thread "main" java.lang.NullPointerException

正确做法

先过滤掉 null 值:

names.stream()
    .filter(Objects::nonNull)
    .map(String::toUpperCase)
    .forEach(System.out::println);

处理 Optional

如果你有 Stream<Optional<T>>,请使用 flatMap(Optional::stream)(Java 9+)来“展开”:

List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty(), Optional.of("B"));
optionals.stream()
    .flatMap(Optional::stream)
    .forEach(System.out::println); // 只会输出 "A" 和 "B"

建议: 尽量不要把 null 放进集合。如果实在避免不了,请在开始就把它们过滤掉。

5. 错误:元素顺序丢失

某些操作(例如 flatMapconcat)可能影响元素顺序,尤其是在使用并行流或合并不同集合时。

错误示例

List<Integer> list1 = List.of(1, 2, 3);
List<Integer> list2 = List.of(4, 5, 6);
Stream<Integer> combined = Stream.concat(list1.stream(), list2.stream());
combined.forEach(System.out::print); // 123456 —— 顺序被保留

// 但如果使用并行流:
Stream<Integer> par = Stream.concat(list1.parallelStream(), list2.parallelStream());
par.forEach(System.out::print); // 顺序可能出乎意料!

正确做法

如果顺序很重要——避免并行流,或使用能保证顺序的方法(forEachOrdered):

par.forEachOrdered(System.out::print);

事实: Stream API 默认会保留顺序,除非你自己把它“打乱”了。

6. 错误:低效地合并集合

进行集合类运算时最好使用 Set 而不是 List,以避免重复并提高性能。在可以使用 flatMap 的地方写嵌套循环也很低效。

错误示例

List<String> list1 = List.of("a", "b", "c");
List<String> list2 = List.of("b", "c", "d");

// 低效的做法:
List<String> union = new ArrayList<>(list1);
for (String s : list2) {
    if (!union.contains(s)) { // List 的 contains 是 O(n)
        union.add(s);
    }
}

// 使用 Set 和 Stream API 的高效做法:
Set<String> unionSet = Stream.concat(list1.stream(), list2.stream())
    .collect(Collectors.toSet());

或者用于差集:

// 低效的做法:
List<String> diff = new ArrayList<>(list1);
diff.removeAll(list2); // OK,但如果你用 stream 并且依赖 list.contains,就会很慢

// 高效的做法:
Set<String> set2 = new HashSet<>(list2);
List<String> difference = list1.stream()
    .filter(s -> !set2.contains(s))
    .collect(Collectors.toList());

建议: 处理唯一元素时尽量使用 Set。对于大集合——避免嵌套循环,优先考虑 stream + flatMap

7. 错误:对同一流重复执行终止操作

有时想把一个流“一分为二”,例如一边收集成列表,一边统计数量。但流是“一次性的”,第二个终止操作会报错。

错误示例

Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
long count = stream.count(); // 砰!IllegalStateException

正确做法

如果需要多次使用其数据——先把它收集到集合:

List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList());
long count = list.size();

建议: 如果既想两头兼顾、又要对同一个流做两件事——先收集到集合。

8. 错误:忘记关闭与资源关联的流

如果你使用与资源关联的流(例如 Files.lines(Path)),请在使用后别忘了关闭它们,以避免泄漏。

错误示例

Stream<String> lines = Files.lines(Path.of("file.txt"));
lines.forEach(System.out::println); // OK,但资源未关闭!

正确做法

使用 try-with-resources:

try (Stream<String> lines = Files.lines(Path.of("file.txt"))) {
    lines.forEach(System.out::println);
}

提示: 文件流就像水壶:烧完就关!

9. 错误:在 flatMap 中对 Optional 与 null 处理不当

处理 List<Optional<T>> 时,很多人会用 map(Optional::get),如果 Optional 是空的就会抛异常。

错误示例

List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty());
optionals.stream()
    .map(Optional::get) // 砰!NoSuchElementException
    .forEach(System.out::println);

正确做法

使用 flatMap(Optional::stream)(Java 9+):

optionals.stream()
    .flatMap(Optional::stream)
    .forEach(System.out::println); // 只有 "A"

建议: Optional 不只是一个“包装”,而是用于安全处理缺失值的工具。

10. 错误:错误实现 equals/hashCode

集合运算(Setdistinct、并集、交集)依赖于正确实现 equalshashCode

错误示例

class Student {
    String name;
    int age;
    // 没有 equals 和 hashCode!
}

Set<Student> students = new HashSet<>();
students.add(new Student("Alice", 20));
students.add(new Student("Alice", 20));
System.out.println(students.size()); // 2,但它们其实应该被视为相同!

正确做法

如果计划在 SetMap 中使用,或要进行 distinct 操作,请为你的类重写 equalshashCode

11. 错误:在 collect 之后继续使用同一个 stream

在执行终止操作(例如 collect)之后,流就关闭了。任何尝试继续使用同一个流都会导致错误。

错误示例

Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
stream.filter(s -> s.startsWith("a")).forEach(System.out::println); // Exception!

正确做法

从集合创建一个新的流:

list.stream().filter(s -> s.startsWith("a")).forEach(System.out::println);

12. 错误:误用 parallelStream

很多人以为 parallelStream() 总能加速。实际上,对于小集合或简单操作,由于线程管理开销,它反而可能更慢。

错误示例

List<Integer> numbers = IntStream.range(0, 10).boxed().collect(Collectors.toList());
numbers.parallelStream().forEach(System.out::println); // 可能比普通的 stream() 更慢

正确做法

仅在数据量很大或计算很重时使用 parallelStream,尤其当操作是纯函数(无副作用)且满足结合律时。

13. 错误:忘记调用终止操作

中间操作(mapfilterflatMap 等)在没有终止操作(collectforEachcount 等)时不会执行。

错误示例

Stream<String> stream = Stream.of("a", "b", "c").map(String::toUpperCase);
// 什么都不会发生!

正确做法

务必调用一个终止操作:

stream.forEach(System.out::println);
1
调查/小测验
并集和投影第 32 级,课程 4
不可用
并集和投影
Stream API:并集和投影
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION