Stream API

什么是Stream

一句话理解:Stream就是数据的流水线加工,把集合里的数据一个个送上传送带,经过过滤、转换、排序等操作,最后收集结果

注意:这个Stream跟IO流(InputStream/OutputStream)完全没关系,别搞混了

Stream三步曲:创建流 → 中间操作(加工) → 终端操作(收结果)

Lambda是Stream的基础,不熟悉Lambda的先看 Lambda表达式

创建Stream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testCreateStream() {
System.out.println("=== 创建Stream ===");

// 1. 从集合创建(最常用)
List<String> list = Arrays.asList("Java", "Python", "Go");
Stream<String> stream1 = list.stream();

// 2. 从数组创建
int[] arr = {1, 2, 3, 4, 5};
IntStream stream2 = Arrays.stream(arr);

// 3. 直接指定元素
Stream<String> stream3 = Stream.of("A", "B", "C");

// 4. 生成无限流(需要配合limit使用,否则停不下来)
Stream<Integer> stream4 = Stream.iterate(0, n -> n + 2).limit(5);
stream4.forEach(n -> System.out.print(n + " ")); // 0 2 4 6 8
System.out.println();
}

中间操作(懒执行)

中间操作不会立刻执行,只是在”攒计划”,等终端操作来了才一起干活

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Test
public void testMiddleOperations() {
System.out.println("=== 中间操作 ===");

List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5, 3);

// filter:过滤,保留满足条件的
numbers.stream()
.filter(n -> n > 3)
.forEach(n -> System.out.print(n + " ")); // 4 5 9 6 5
System.out.println();

// map:转换,把每个元素变成另一个东西
List<String> names = Arrays.asList("alice", "bob", "charlie");
names.stream()
.map(String::toUpperCase)
.forEach(s -> System.out.print(s + " ")); // ALICE BOB CHARLIE
System.out.println();

// sorted:排序
numbers.stream()
.sorted()
.forEach(n -> System.out.print(n + " ")); // 1 1 2 3 3 4 5 5 6 9
System.out.println();

// distinct:去重
numbers.stream()
.distinct()
.forEach(n -> System.out.print(n + " ")); // 3 1 4 5 9 2 6
System.out.println();

// limit:取前N个
numbers.stream()
.limit(3)
.forEach(n -> System.out.print(n + " ")); // 3 1 4
System.out.println();

// skip:跳过前N个
numbers.stream()
.skip(7)
.forEach(n -> System.out.print(n + " ")); // 6 5 3
System.out.println();
}

flatMap:把嵌套结构”拍平”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testFlatMap() {
System.out.println("=== flatMap ===");

// 场景:每个学生选了多门课,想拿到所有课程的列表
List<List<String>> courses = Arrays.asList(
Arrays.asList("数学", "英语"),
Arrays.asList("物理", "化学"),
Arrays.asList("历史", "地理")
);

// map的话,得到的是 Stream<List<String>>,还是嵌套的
// flatMap 直接拍平成 Stream<String>
courses.stream()
.flatMap(Collection::stream)
.forEach(c -> System.out.print(c + " "));
// 数学 英语 物理 化学 历史 地理
}
操作 作用 示例
filter 过滤 .filter(n -> n > 5)
map 转换 .map(String::toUpperCase)
sorted 排序 .sorted().sorted(Comparator)
distinct 去重 .distinct()
limit 取前N个 .limit(3)
skip 跳过前N个 .skip(2)
flatMap 拍平嵌套结构 .flatMap(Collection::stream)
peek 调试用,不影响流 .peek(System.out::println)

终端操作(触发执行)

终端操作是”收割”步骤,调用之后整个流水线才真正跑起来

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
30
31
32
33
34
35
36
37
@Test
public void testTerminalOperations() {
System.out.println("=== 终端操作 ===");

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// forEach:遍历每个元素
numbers.stream().forEach(n -> System.out.print(n + " "));
System.out.println();

// collect:收集结果到集合(最常用)
List<Integer> evenList = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println("偶数列表:" + evenList); // [2, 4, 6, 8, 10]

// count:计数
long count = numbers.stream().filter(n -> n > 5).count();
System.out.println("大于5的个数:" + count); // 5

// reduce:归约,把所有元素合并成一个结果
int sum = numbers.stream().reduce(0, Integer::sum);
System.out.println("求和:" + sum); // 55

// min / max
Optional<Integer> min = numbers.stream().min(Integer::compareTo);
Optional<Integer> max = numbers.stream().max(Integer::compareTo);
System.out.println("最小值:" + min.get() + ",最大值:" + max.get());

// anyMatch / allMatch / noneMatch
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0);
boolean allPositive = numbers.stream().allMatch(n -> n > 0);
boolean noNegative = numbers.stream().noneMatch(n -> n < 0);
System.out.println("有偶数?" + hasEven); // true
System.out.println("全是正数?" + allPositive); // true
System.out.println("没有负数?" + noNegative); // true
}

关于 Optional类 的用法,min/max 返回的就是Optional

Collectors工具类

Collectors是收集器的工具箱,配合 collect() 使用

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
30
31
32
33
34
@Test
public void testCollectors() {
System.out.println("=== Collectors ===");

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

// toList:收集到List
List<String> list = names.stream().distinct().collect(Collectors.toList());
System.out.println("去重列表:" + list);

// toSet:收集到Set(自动去重)
Set<String> set = names.stream().collect(Collectors.toSet());
System.out.println("Set:" + set);

// joining:拼接字符串
String joined = names.stream().collect(Collectors.joining(", "));
System.out.println("拼接:" + joined); // Alice, Bob, Charlie, Alice, David

// toMap:收集到Map
// ⚠️ 注意:key不能重复,否则会报错!重复时需要指定合并策略
Map<String, Integer> nameLength = names.stream()
.distinct()
.collect(Collectors.toMap(
name -> name,
name -> name.length()
));
System.out.println("名字长度Map:" + nameLength);

// groupingBy:分组(SQL里的GROUP BY)
Map<Integer, List<String>> grouped = names.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println("按长度分组:" + grouped);
// {3=[Bob], 5=[Alice, Alice, David], 7=[Charlie]}
}
Collector 作用 示例
toList() 收集到List .collect(Collectors.toList())
toSet() 收集到Set .collect(Collectors.toSet())
toMap() 收集到Map .collect(Collectors.toMap(k, v))
groupingBy() 分组 .collect(Collectors.groupingBy(分组条件))
joining() 拼接字符串 .collect(Collectors.joining(", "))
counting() 分组计数 groupingBy(xx, counting())
summarizingInt() 统计(sum/avg/min/max) .collect(Collectors.summarizingInt(xx))

实战案例:学生列表处理

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Test
public void testStudentStream() {
System.out.println("=== 学生列表Stream实战 ===");

// 准备数据
List<Student> students = Arrays.asList(
new Student("张三", 85, "计算机"),
new Student("李四", 92, "数学"),
new Student("王五", 78, "计算机"),
new Student("赵六", 95, "数学"),
new Student("孙七", 60, "英语"),
new Student("周八", 88, "计算机"),
new Student("吴九", 70, "英语")
);

// 1. 找出分数大于80的学生姓名
List<String> highScoreNames = students.stream()
.filter(s -> s.getScore() > 80)
.map(Student::getName)
.collect(Collectors.toList());
System.out.println("80分以上:" + highScoreNames); // [张三, 李四, 赵六, 周八]

// 2. 按分数降序排列
List<Student> sorted = students.stream()
.sorted((s1, s2) -> s2.getScore() - s1.getScore())
.collect(Collectors.toList());
sorted.forEach(s -> System.out.println(s.getName() + ": " + s.getScore()));

// 3. 按专业分组
Map<String, List<Student>> byDept = students.stream()
.collect(Collectors.groupingBy(Student::getDepartment));
byDept.forEach((dept, list) -> {
System.out.println(dept + ": " + list.size() + "人");
});

// 4. 求每个专业的平均分
Map<String, Double> avgByDept = students.stream()
.collect(Collectors.groupingBy(
Student::getDepartment,
Collectors.averagingInt(Student::getScore)
));
System.out.println("各专业平均分:" + avgByDept);

// 5. 找出最高分的学生
Optional<Student> topStudent = students.stream()
.max(Comparator.comparingInt(Student::getScore));
topStudent.ifPresent(s -> System.out.println("最高分:" + s.getName() + " " + s.getScore()));
}

并行流 parallelStream

并行流会把数据拆成多块,用多个线程同时处理,理论上更快

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
@Test
public void testParallelStream() {
System.out.println("=== 并行流 ===");

List<Integer> bigList = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
bigList.add(i);
}

// 串行流
long start1 = System.currentTimeMillis();
long count1 = bigList.stream()
.filter(n -> n % 2 == 0)
.count();
long time1 = System.currentTimeMillis() - start1;

// 并行流
long start2 = System.currentTimeMillis();
long count2 = bigList.parallelStream()
.filter(n -> n % 2 == 0)
.count();
long time2 = System.currentTimeMillis() - start2;

System.out.println("串行:" + time1 + "ms,结果:" + count1);
System.out.println("并行:" + time2 + "ms,结果:" + count2);
}

什么时候用并行流

✅ 数据量大(几万条以上)

✅ 每个元素的处理比较耗时(比如网络请求、复杂计算)

✅ 操作无状态、无副作用

什么时候不要用并行流

❌ 数据量小(并行的线程调度开销可能比计算本身还大)

❌ 操作有顺序要求(并行流不保证顺序)

❌ 操作涉及共享可变状态(会出线程安全问题)

❌ 数据源是LinkedList(拆分效率低,ArrayList更适合并行)

常见坑

坑1:Stream只能消费一次!

1
2
3
4
5
6
7
@Test
public void testStreamOnce() {
Stream<String> stream = Stream.of("A", "B", "C");
stream.forEach(System.out::println); // 正常
// stream.forEach(System.out::println); // ❌ 报错!IllegalStateException: stream has already been operated upon
// 想再用就得重新创建Stream
}

坑2:中间操作是懒执行的,没有终端操作就不会执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testLazy() {
List<String> names = Arrays.asList("Alice", "Bob");
// 下面这行什么都不会打印!因为没有终端操作
names.stream().filter(n -> {
System.out.println("过滤:" + n); // 不会执行
return true;
});
// 加上终端操作才会执行
names.stream().filter(n -> {
System.out.println("过滤:" + n); // 会执行
return true;
}).collect(Collectors.toList());
}

坑3:在forEach里修改外部集合可能出问题

1
2
3
4
5
6
// ❌ 千万别这么干
List<String> result = new ArrayList<>();
list.parallelStream().forEach(s -> result.add(s)); // 线程不安全!

// ✅ 用collect
List<String> result = list.parallelStream().collect(Collectors.toList());

坑4:toMap的key重复会抛异常

1
2
3
4
5
6
7
8
9
// ❌ 如果有重复key,直接报 IllegalStateException
list.stream().collect(Collectors.toMap(Student::getDepartment, Student::getName));

// ✅ 指定key冲突时的合并策略
list.stream().collect(Collectors.toMap(
Student::getDepartment,
Student::getName,
(old, newVal) -> old + "," + newVal // 冲突时拼接
));

练习题

题1:给定一个整数列表,用Stream找出所有偶数,排序后收集到新列表

题2:给定字符串列表 ["hello", "world", "hello", "java"],用Stream统计每个字符串出现的次数(提示:groupingBy + counting

题3:用Stream把一个 List<String> 转成一个逗号分隔的字符串

题4:用flatMap把 List<String> 中每个字符串拆成字符流,统计不重复字符的个数

提示:str.chars()str.split("")

题5:用reduce实现一个列表中所有数字的乘积


上一章 目录 下一章
Lambda表达式 java基础 Optional类