跳到主要内容

Stream API

问题

Java Stream API 有哪些常用操作?中间操作和终端操作有什么区别?并行流需要注意什么?

答案

Stream 基础

Stream 是 Java 8 引入的集合数据处理管道,支持函数式风格的链式操作:

StreamBasic.java
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");

// 过滤 → 转换 → 收集
List<String> result = names.stream()
.filter(name -> name.length() > 3) // 中间操作:过滤
.map(String::toUpperCase) // 中间操作:转换
.sorted() // 中间操作:排序
.collect(Collectors.toList()); // 终端操作:收集
// [ALICE, CHARLIE, DAVID]
Stream 的特点
  • 惰性求值:中间操作不会立即执行,直到遇到终端操作才触发
  • 一次性:Stream 只能消费一次,不能重复使用
  • 不修改源数据:所有操作都生成新的 Stream

中间操作 vs 终端操作

分类方法说明
中间操作filter(Predicate)过滤
map(Function)一对一转换
flatMap(Function)一对多转换(展平)
sorted() / sorted(Comparator)排序
distinct()去重(基于 equals/hashCode)
limit(n) / skip(n)截取/跳过
peek(Consumer)调试(不修改元素)
终端操作collect(Collector)收集到集合
forEach(Consumer)遍历
reduce(BinaryOperator)归约
count()计数
anyMatch/allMatch/noneMatch匹配判断
findFirst/findAny查找元素
toArray()转为数组

常用操作示例

StreamOperations.java
List<Person> people = List.of(
new Person("Alice", 30, "Engineering"),
new Person("Bob", 25, "Marketing"),
new Person("Charlie", 35, "Engineering"),
new Person("David", 28, "Marketing"),
new Person("Eve", 32, "Engineering")
);

// flatMap:展平嵌套集合
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4));
List<Integer> flat = nested.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList()); // [1, 2, 3, 4]

// reduce:归约
int sum = List.of(1, 2, 3, 4, 5).stream()
.reduce(0, Integer::sum); // 15

Optional<Integer> max = List.of(1, 2, 3).stream()
.reduce(Integer::max); // Optional[3]

// groupingBy:分组
Map<String, List<Person>> byDept = people.stream()
.collect(Collectors.groupingBy(Person::department));

// groupingBy + counting:分组计数
Map<String, Long> countByDept = people.stream()
.collect(Collectors.groupingBy(Person::department, Collectors.counting()));

// partitioningBy:二分(按条件分为两组)
Map<Boolean, List<Person>> partition = people.stream()
.collect(Collectors.partitioningBy(p -> p.age() > 30));

// joining:字符串连接
String names = people.stream()
.map(Person::name)
.collect(Collectors.joining(", ", "[", "]")); // [Alice, Bob, Charlie, David, Eve]

// toMap:转为 Map
Map<String, Integer> nameAge = people.stream()
.collect(Collectors.toMap(Person::name, Person::age));

// summarizingInt:统计信息
IntSummaryStatistics stats = people.stream()
.collect(Collectors.summarizingInt(Person::age));
stats.getAverage(); // 30.0
stats.getMax(); // 35
stats.getMin(); // 25
stats.getSum(); // 150
stats.getCount(); // 5

并行流

ParallelStream.java
// 创建并行流
List<Integer> numbers = IntStream.rangeClosed(1, 1000000)
.boxed().collect(Collectors.toList());

// 方式一:直接创建
long count = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.count();

// 方式二:从串行流转换
long count = numbers.stream()
.parallel()
.filter(n -> n % 2 == 0)
.count();
并行流注意事项
  1. 线程安全:不要在并行流中修改共享变量
  2. 数据源:ArrayList 并行效率高(可拆分),LinkedList 效率低
  3. 操作类型:无状态操作(filter、map)适合并行,有状态操作(sorted、distinct)不适合
  4. 数据量:小数据量并行可能更慢(线程创建/合并开销)
  5. ForkJoinPool:并行流默认使用公共的 ForkJoinPool,长时间操作可能阻塞其他并行任务

常见面试问题

Q1: Stream 和 Collection 的区别?

答案

维度CollectionStream
用途存储数据处理数据
执行时机立即执行惰性求值
可复用可以多次遍历只能消费一次
修改数据可以增删改不修改源数据
优化短路优化、并行化

Q2: map()flatMap() 的区别?

答案

  • map():一对一,将每个元素转换为另一个元素
  • flatMap():一对多,将每个元素转换为一个 Stream,然后把所有 Stream 合并(展平)
// map:每个字符串转为字符数组 → Stream<String[]>
List<String[]> result = words.stream()
.map(word -> word.split(""))
.collect(Collectors.toList()); // [["H","i"], ["B","y","e"]]

// flatMap:展平 → Stream<String>
List<String> result = words.stream()
.flatMap(word -> Arrays.stream(word.split("")))
.collect(Collectors.toList()); // ["H","i","B","y","e"]

Q3: Stream 是惰性求值吗?怎么理解?

答案

是的。中间操作只是记录操作链,不会立即执行。直到遇到终端操作时,才从头执行所有操作:

// 不会打印任何东西!因为没有终端操作
Stream.of(1, 2, 3, 4, 5)
.filter(n -> {
System.out.println("filter: " + n);
return n > 2;
})
.map(n -> {
System.out.println("map: " + n);
return n * 2;
});
// 加上 .collect(Collectors.toList()) 才会执行

而且 Stream 会进行短路优化findFirst()anyMatch() 找到结果后立即停止处理。

Q4: Collectors.toList()Stream.toList() 的区别?

答案

维度Collectors.toList()Stream.toList()(JDK 16+)
结果可变 ArrayList不可变 List
null 元素允许允许
版本JDK 8+JDK 16+

Q5: 什么情况下不适合使用 Stream?

答案

  1. 简单遍历:普通 for 循环更直观
  2. 需要修改元素:Stream 不应有副作用
  3. 性能敏感:Stream 有创建对象和方法调用的开销
  4. 复杂逻辑:多层嵌套的 Stream 可读性反而差
  5. 小数据量的并行流:线程开销远大于计算开销

相关链接