Java中Stream流完全指南

Java 8 把 Stream 加进来之后,处理集合的方式跟以前不太一样了。你可能也写过 list.stream().filter(...).collect(...),但中间操作、终止操作、收集器这几个东西搅在一起时,还是容易写错。

这篇把 Java 8 里常用到的 Stream 用法过一遍,每个都配上能直接跑的例子。并行流和常见的坑也一并说一下。

一、Stream 是什么

先说它不存数据这件事:

  • 它不是集合,只是数据的管道。
  • 它没有下标,不能随机访问。
  • 它不动源数据,每次操作都产生新结果。

一条 Stream 跑起来就是三步:

1
数据源 → 中间操作(0~N个) → 终止操作(1个)

举个最基础的例子:

1
2
3
4
5
6
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> result = names.stream() // 数据源
.filter(n -> n.length() > 3) // 中间操作
.map(String::toUpperCase) // 中间操作
.collect(Collectors.toList()); // 终止操作
// 结果: [ALICE, CHARLIE]

有两个地方容易被坑:

  1. 惰性求值:filter、map 这些中间操作不会马上执行,得等 collect、count 这种终止操作出现,整条链才真正跑。
  2. 一次性:一个 Stream 对象只能被消费一次。第二次调用终止操作会抛 IllegalStateException: stream has already been operated upon or closed

二、怎么建一个 Stream

数据源来源挺多的,常用的就这几类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 1. 集合(最常用)
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> s1 = list.stream(); // 串行
Stream<String> s2 = list.parallelStream(); // 并行

// 2. 数组
String[] arr = {"a", "b", "c"};
Stream<String> s3 = Arrays.stream(arr);

// 3. 直接 of(可变参数)
Stream<String> s4 = Stream.of("a", "b", "c");

// 4. 迭代生成(Java 8 需要 limit 截断)
Stream<Integer> s5 = Stream.iterate(0, n -> n + 2).limit(5); // 0,2,4,6,8

// 5. 无限生成(常用于造测试数据)
Stream<Double> s6 = Stream.generate(Math::random).limit(3);

// 6. 原始类型流 range / rangeClosed
IntStream s7 = IntStream.range(0, 5); // 0,1,2,3,4(不含上界)
IntStream s8 = IntStream.rangeClosed(0, 5); // 0,1,2,3,4,5(含上界)

// 7. 读取文件每行(自动处理 IO 和关闭)
try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {
lines.forEach(System.out::println);
}

// 8. 空流
Stream<String> s9 = Stream.empty();

三、中间操作

中间操作返回的是新 Stream,而且是惰性的:不碰到终止操作,它什么都不干。

无状态操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// filter:按条件保留
stream.filter(x -> x > 0)

// map:一对一转换
stream.map(String::toUpperCase) // T -> R
stream.mapToInt(Integer::intValue) // T -> int(转原始流,避免装箱)

// flatMap:一对多"拍平",常用于处理嵌套集合
List<List<Integer>> nested = Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3, 4));
List<Integer> flat = nested.stream()
.flatMap(List::stream) // List<Integer> -> Integer 流
.collect(Collectors.toList()); // [1,2,3,4]

// 字符串拆词也是经典场景
List<String> words = lines.stream()
.flatMap(line -> Arrays.stream(line.split(" ")))
.collect(Collectors.toList());

// peek:偷偷看一眼,主要用于调试(注意:不触发终止操作就不会执行)
stream.peek(System.out::println)

有状态操作

1
2
3
4
5
6
7
8
9
10
11
12
// distinct:去重(依赖 equals/hashCode)
stream.distinct()

// sorted:排序,无参用自然序,可传 Comparator
stream.sorted()
stream.sorted(Comparator.comparing(Person::getAge).reversed())

// limit:截取前 N 个
stream.limit(10)

// skip:跳过前 N 个
stream.skip(5)

sorteddistinct 这类有状态操作,在并行流里需要各线程之间同步状态,开销不小。数据量不大的时候,别为了好看硬上并行。

四、终止操作

终止操作会真正触发整条链执行,而且一条流上只能有一个。

遍历与收集

1
2
3
4
5
6
7
8
9
10
// forEach:遍历,并行时不保证顺序
stream.forEach(System.out::println);
// forEachOrdered:并行时也按原序输出(牺牲并行性能)
stream.parallel().forEachOrdered(System.out::println);

// collect:把结果收进容器(最常用,下一节细讲)
List<String> list = stream.collect(Collectors.toList());

// toArray:转数组
String[] arr = stream.toArray(String[]::new);

聚合与归约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// count:计数
long n = stream.count();

// reduce:把一串元素"折叠"成单个结果,典型场景是求和、求积、拼接、找极值。
// 核心机制:手里有个累加器,从初始值(种子)开始,每遇到一个元素就把它合并进去。

// 重载一:带初始值,元素类型 == 结果类型(BinaryOperator<T> 即 (T,T)->T)
// 串行演算:0 → 0+1=1 → 1+2=3 → 3+3=6 → 6+4=10
int sum1 = Arrays.asList(1, 2, 3, 4).stream().reduce(0, Integer::sum);
// ↑初始值 ↑累加器:(当前和, 元素) -> 新和
// 结果一定非 null(至少返回初始值 0),所以返回 int 而不是 Optional

// 重载二:不带初始值,拿第一个元素当种子
// 非空流:seed=1 → 1+2=3 → 3+3=6 → 6+4=10 → Optional[10]
// 空流:没有第一个元素 → Optional.empty()
Optional<Integer> sum2 = Arrays.asList(1, 2, 3, 4).stream().reduce(Integer::sum);

// 重载三:结果类型可以和元素类型不同,并且支持并行
// 把若干字符串"折"成它们的总长度(元素是 String,结果是 int)
int totalLen = Arrays.asList("ab", "cde", "fg").stream().reduce(
0, // 初始值(int)
(len, word) -> len + word.length(), // 累加器:(当前总长度, 元素) -> 新总长度
Integer::sum); // 合并器:并行时把两个分片的长度相加
// 串行演算:0+2=2 → 2+3=5 → 5+2=7 → 7

关于第三个重载的 combiner:串行执行时它不参与计算;一旦 .parallel(),流会被切成多段,每段各自用 accumulator 算出局部结果,最后用 combiner 把各段结果合成一个。所以 combiner 不是装饰,是并行正确性的保证。

有个坑得留意:初始值(identity)必须是”中性值”,即满足 combiner.apply(identity, x) == x。求和用 0、求积用 1、拼接用 ""。如果求和误把初始值写成 10,并行时每个分片都从 10 起步,合并后会多出”分片数个 10”,结果就错了。

reduce 与 collect 的分工:reduce 适合”聚合成一个标量或简单对象”(数字、字符串、自定义累加器);”收集成集合 / 分组 / 转 Map”请用 collect + Collectors,那是它的主场,别硬用 reduce 拼 List(既慢又啰嗦)。

匹配与查找

1
2
3
4
5
6
7
8
// 匹配:返回 boolean,遇到结果立即短路
boolean any = stream.anyMatch(x -> x > 10); // 有一个满足
boolean all = stream.allMatch(x -> x > 0); // 全满足
boolean none = stream.noneMatch(x -> x < 0); // 全不满足

// 查找:返回 Optional
Optional<String> first = stream.findFirst(); // 第一个(并行下也稳定)
Optional<String> anyOne = stream.findAny(); // 任意一个(并行下更快)

极值

1
2
3
4
Optional<Integer> max = numbers.stream().max(Integer::compareTo);
Optional<Integer> min = numbers.stream().min(Integer::compareTo);
// 也可以用原始流,省去 Optional
int maxVal = numbers.stream().mapToInt(Integer::intValue).max().orElse(0);

五、Collectors 收集器

collectCollectors 基本上包揽了所有的收尾工作,也是平时写 Stream 用得最频繁的一块。

收集到集合

1
2
3
4
5
6
7
8
9
10
// 转 List / Set
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());

// 想要具体实现类(比如 LinkedHashSet 保持插入序)
Set<String> linked = stream.collect(Collectors.toCollection(LinkedHashSet::new));

// 想要不可变集合,用 Collections.unmodifiableXXX 包一层(Java 8 标准做法)
List<String> immutable = Collections.unmodifiableList(
stream.collect(Collectors.toList()));

转 Map

转 Map 是最容易踩坑的地方:

1
2
3
4
5
6
7
8
9
10
11
// 基础版:key 重复会抛 IllegalStateException
Map<String, Integer> m1 = people.stream()
.collect(Collectors.toMap(Person::getName, Person::getAge));

// 解决重复 key:第三个参数是"合并函数",决定重复时留哪个
Map<String, Integer> m2 = people.stream()
.collect(Collectors.toMap(Person::getName, Person::getAge, (oldV, newV) -> newV));

// 收集成对象本身当 value
Map<String, Person> m3 = people.stream()
.collect(Collectors.toMap(Person::getName, Function.identity()));

分组与分区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 按某个属性分组(最常用)
Map<Integer, List<Person>> byAge = people.stream()
.collect(Collectors.groupingBy(Person::getAge));

// 多级分组
Map<Integer, Map<String, List<Person>>> byAgeThenName = people.stream()
.collect(Collectors.groupingBy(Person::getAge,
Collectors.groupingBy(Person::getName)));

// 分组后做下游聚合(而不是收集成 List)
Map<Integer, Long> countByAge = people.stream()
.collect(Collectors.groupingBy(Person::getAge, Collectors.counting()));

Map<Integer, Double> avgScore = students.stream()
.collect(Collectors.groupingBy(Student::getClassId,
Collectors.averagingInt(Student::getScore)));

// 分区:特殊的二分组(true / false)
Map<Boolean, List<Person>> adults = people.stream()
.collect(Collectors.partitioningBy(p -> p.getAge() >= 18));

统计与拼接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 数值统计三件套
long count = stream.collect(Collectors.counting());
double avg = stream.collect(Collectors.averagingInt(Person::getAge));
int sum = stream.collect(Collectors.summingInt(Person::getAge));

// summarizing:一次性拿到 count/sum/avg/min/max
IntSummaryStatistics stat = people.stream()
.collect(Collectors.summarizingInt(Person::getAge));
// stat.getMax(), getMin(), getAverage()...

// 极值
Optional<Person> oldest = people.stream()
.collect(Collectors.maxBy(Comparator.comparing(Person::getAge)));

// 拼接字符串
String joined = stream.collect(Collectors.joining()); // 无分隔
String csv = stream.collect(Collectors.joining(", ")); // 逗号分隔
String pretty = stream.collect(Collectors.joining(", ", "[", "]")); // [a, b, c]

进阶:mapping 与 collectingAndThen

1
2
3
4
5
6
7
8
9
// groupingBy 后只想收集某个字段
Map<Integer, List<String>> namesByAge = people.stream()
.collect(Collectors.groupingBy(Person::getAge,
Collectors.mapping(Person::getName, Collectors.toList())));

// collectingAndThen:收集完再包一层转换(常用于转不可变集合)
List<String> safe = stream.collect(Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList));

六、Optional 与 Stream 的配合

凡是”可能不存在”的结果(findFirst、max、min、reduce 无初值版),返回的都是 Optional。别再用 null 到处判空了:

1
2
3
4
5
6
7
8
Optional<Person> top = people.stream()
.max(Comparator.comparing(Person::getScore));

// 正确姿势:用 Optional 自带方法处理"空"的情况
top.ifPresent(p -> System.out.println(p.getName())); // 有才打印
Person p1 = top.orElse(defaultPerson); // 没有给默认值
Person p2 = top.orElseGet(() -> loadDefault()); // 懒加载默认值
Person p3 = top.orElseThrow(() -> new RuntimeException("没人")); // 没有就抛

七、并行流

一行 parallel() 就把串行流切到并行:

1
2
3
4
5
6
long count = list.parallelStream()
.filter(s -> s.length() > 3)
.count();

// 也可以中途切换
list.stream().parallel().filter(...).sequential().map(...);

底层用 ForkJoinPool.commonPool(),把数据分片,多个线程各算一段,最后合并。

适合用并行的情况:

  • 数据量大,得上万、百万级才能看出收益。
  • 每个元素的计算是 CPU 密集且互相独立的。
  • 中间没有 sorteddistinct 这类需要跨线程同步状态的操作,否则并行优势基本被抵消。

不适合的情况:

  • 数据量小,线程调度的开销比计算本身还大。
  • IO 密集(网络、数据库),会占满公共线程池,拖垮整个 JVM。
  • 操作里碰了共享可变状态,并行读写不加锁就是 bug。
1
2
3
4
5
6
7
// 错误示范:并行里改外部累加器(线程不安全)
List<Integer> nums = ...;
int[] sum = {0};
nums.parallelStream().forEach(n -> sum[0] += n); // 结果不可预测!

// 正确做法:交给 reduce / 原始流
int total = nums.parallelStream().mapToInt(Integer::intValue).sum();

findAny 在并行下随便返回一个,比 findFirst 快;如果你不在乎顺序,用 findAny

八、常见的坑

1. 流只能消费一次

1
2
3
Stream<String> s = list.stream();
s.count();
s.count(); // 抛 IllegalStateException

解决办法:每次用都重新 list.stream(),或者先把结果 collect 成集合。

2. 中间操作是惰性的,peek 不触发

1
2
3
4
5
6
7
8
list.stream()
.peek(System.out::println) // 这行不会打印!
.filter(x -> x > 0); // 没有终止操作,啥都不执行

list.stream()
.peek(System.out::println)
.filter(x -> x > 0)
.collect(Collectors.toList()); // 加上终止操作,peek 才会跑

3. 自动装箱的性能问题

1
2
3
4
5
// 慢:每个 Integer 都要装箱/拆箱
int sum = list.stream().map(x -> x * 2).reduce(0, Integer::sum);

// 快:用原始类型流
int sum = list.stream().mapToInt(x -> x * 2).sum();

处理大量数值时,用 IntStream / LongStream / DoubleStream 避开装箱。

4. 流里改外部变量会编译失败

1
2
int total = 0;
list.stream().forEach(x -> total += x); // 编译错误:变量必须是 final 或 effectively final

函数式接口要求捕获的变量不可变。要聚合就走 reduce / collect,别想着在外面改个累加变量。

5. 流里出现 null 元素

1
2
3
4
List<String> list = Arrays.asList("a", null, "b");
list.stream().map(String::length); // 碰上 null 就 NPE
// 防御:先 filter 掉 null
list.stream().filter(Objects::nonNull).map(String::length)

源头就在 map 前先 .filter(Objects::nonNull) 把 null 剔掉。

6. toMap 的重复 key

前面写过,重复 key 默认抛异常,记得传合并函数。

7. forEach 顺序和并行

并行流里 forEach 不保证顺序,forEachOrdered 才保序,但会拖慢并行。想要有序输出,不如直接串行。

九、几条实践建议

  1. 数值处理用原始类型流:int/long/double 走 IntStream 等,少装箱。
  2. 短路操作往前放:filter 放 map 前面,先把数据量压下来再转换。
  3. 别在流里搞副作用:不要在 forEach / peek 里改外部状态,结果用 collect / reduce 拿。
  4. sorted 加并行要谨慎:真要排序又数据量大,先想清楚是不是非并行不可。
  5. 不是所有循环都该换成 Stream:几行 for、需要 break/continue、逻辑绕来绕去的,传统循环反而清楚。Stream 是让数据转换变优雅,不是要把所有 for 消灭。
  6. 可读性放第一:一条链七八步、flatMap 套 groupingBy 的,拆成中间变量或者分步写,别为了炫全塞一行。

十、一个完整例子

假设有一个 Order 列表,需求:统计每个用户、每个月的订单总金额,并且只保留金额大于 1000 的记录,最后按金额降序输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Order {
String userId;
int month; // 1~12
BigDecimal amount;
// getters...
}

List<Order> orders = loadOrders();

Map<String, Map<Integer, BigDecimal>> result = orders.stream()
// 1. 过滤掉金额过小的订单
.filter(o -> o.getAmount().compareTo(BigDecimal.valueOf(1000)) > 0)
// 2. 按 userId 分组,再按 month 二级分组
.collect(Collectors.groupingBy(Order::getUserId,
Collectors.groupingBy(Order::getMonth,
// 3. 下游用 reducing 做金额求和
Collectors.reducing(
BigDecimal.ZERO,
Order::getAmount,
BigDecimal::add))));

// 输出(顺便按金额降序)
result.forEach((userId, byMonth) -> {
System.out.println("用户 " + userId);
byMonth.entrySet().stream()
.sorted(Map.Entry.<Integer, BigDecimal>comparingByValue().reversed())
.forEach(e -> System.out.println(" " + e.getKey() + "月: " + e.getValue()));
});

这个例子把 filtergroupingBy(多级)→ reducing(下游聚合)→ 再 stream 排序输出串了起来,日常大部分聚合需求都能照这个思路写。

十一、收个尾

说到底就三点:

  • 数据源 + 中间操作(惰性)+ 终止操作(触发)。
  • 无状态操作(filter/map/flatMap)便宜,有状态操作(distinct/sorted/limit)贵,并行时差距更明显。
  • 能 collect 就别搞副作用,能用原始流就别装箱。

Stream 不会让你的程序变快,极端情况反而更慢,但它能让数据处理代码更短、更好读、更不容易出 bug。中间操作和 Collectors 那几个方法多用几次就熟了,并行和自定义收集器等真用到了再查也不晚。


参考资料:Java 8 官方 java.util.stream 包文档。


Java中Stream流完全指南
https://zyue2022.github.io/2026/08/12/Java中Stream流完全指南/
作者
ZYUE
发布于
2026年8月12日
更新于
2026年8月13日
许可协议