Java Stream 流:让数据处理更优雅的 “魔法管道“
作为一名 Java 开发者,我还记得刚接触 Stream 流时的惊喜 —— 原来数据处理可以如此简洁优雅。对于刚学习 Java 的大学生来说,Stream 流可能看起来有点抽象,但一旦掌握,它会成为你代码中的 "瑞士军刀"。今天我们就来聊聊 Stream 流到底是什么,以及它能帮我们解决什么问题。
一、为什么需要 Stream 流?
在没有 Stream 流的年代,我们处理集合数据通常是这样的:
// 传统方式:筛选出年龄大于18的学生并打印名字
List<Student> students = ...;
for (Student s : students) {if (s.getAge() > 18) {System.out.println(s.getName());}
}
这种代码没什么问题,但当需求变得复杂时(比如多步筛选、转换、聚合),代码会变得冗长且可读性差。Stream 流的出现就是为了:
- 简化集合数据的处理逻辑
- 让代码更易读、更易维护
- 支持函数式编程风格
- 轻松实现并行处理(无需手动处理多线程)
二、什么是 Stream 流?
简单来说,Stream 流是 Java 8 引入的一套处理集合数据的 API,它像一个 "数据管道":
- 数据源(通常是集合)的数据流入管道
- 经过一系列中间操作(筛选、转换、排序等)
- 最终通过终止操作得到结果
注意:Stream 流不会改变原始数据源,所有操作都是惰性执行的(直到终止操作才会真正执行)。
三、Stream 流的常用操作
在学习具体方法前,先明确一个重要概念:Stream 的操作分为中间操作和终止操作。
- 中间操作:返回新的 Stream,支持链式调用(惰性执行,只有终止操作触发时才会执行)
- 终止操作:返回具体结果(如集合、数值、布尔值等),触发整个流的计算
记住这个分类,能帮你理解 Stream 的执行逻辑。
假设我们有一个学生列表,包含姓名、年龄、成绩等信息:
class Student {private String name;private int age;private double score;// 构造方法、getter、toString省略
}List<Student> students = Arrays.asList(new Student("张三", 20, 85.5),new Student("李四", 17, 92.0),new Student("王五", 22, 78.5),new Student("赵六", 19, 90.0)
);(一)、常用中间操作(返回 Stream)
1. 筛选与切片
| 方法 | 作用 | 示例 |
|---|---|---|
filter(Predicate<T> p) | 保留满足条件的元素 | stream.filter(s -> s.getAge() > 18) |
limit(long n) | 截取前 n 个元素 | stream.limit(3)(取前 3 个) |
skip(long n) | 跳过前 n 个元素 | stream.skip(2)(跳过前 2 个) |
distinct() | 去重(依赖 equals ()) | stream.distinct() |
实战场景:从学生列表中获取年龄 > 18 的前 2 名(排除重复数据)
List<Student> result = students.stream().filter(s -> s.getAge() > 18).distinct().limit(2).collect(Collectors.toList());
2. 映射(类型转换)
| 方法 | 作用 | 示例 |
|---|---|---|
map(Function<T, R> f) | 将 T 类型转换为 R 类型 | stream.map(Student::getName)(转姓名) |
mapToInt(ToIntFunction<T> f) | 转换为 IntStream | stream.mapToInt(Student::getAge) |
mapToLong(ToLongFunction<T> f) | 转换为 LongStream | stream.mapToLong(User::getId) |
mapToDouble(ToDoubleFunction<T> f) | 转换为 DoubleStream | stream.mapToDouble(Student::getScore) |
flatMap(Function<T, Stream<R>> f) | 将流中每个元素转换为子流并合并 | 见下方示例 |
flatMap 示例:将列表中的字符串拆分为单个字符(如 ["abc", "def"] → ['a','b','c','d','e','f'])
List<String> words = Arrays.asList("abc", "def");
List<Character> chars = words.stream().flatMap(s -> {List<Character> list = new ArrayList<>();for (char c : s.toCharArray()) {list.add(c);}return list.stream(); // 每个字符串转成字符流}).collect(Collectors.toList());
3. 排序
| 方法 | 作用 | 示例 |
|---|---|---|
sorted() | 自然排序(需元素实现 Comparable) | stream.sorted()(如 String、Integer 的默认排序) |
sorted(Comparator<T> c) | 自定义排序 | stream.sorted(Comparator.comparingInt(Student::getAge)) |
示例:按成绩降序排序,成绩相同按年龄升序
students.stream().sorted(Comparator.comparingDouble(Student::getScore).reversed() // 成绩降序.thenComparingInt(Student::getAge)) // 年龄升序.forEach(System.out::println);
4. 其他中间操作
| 方法 | 作用 | 示例 |
|---|---|---|
peek(Consumer<T> c) | 对流中元素执行操作(不改变元素),常用于调试 | stream.peek(s -> System.out.println("处理中:" + s)) |
(二)、常用终止操作(返回具体结果)
1. 遍历与收集
| 方法 | 作用 | 示例 |
|---|---|---|
forEach(Consumer<T> c) | 遍历元素 | stream.forEach(System.out::println) |
collect(Collector c) | 收集流为集合 / 对象 | 见下方详细说明 |
collect 常用收集器(需导入java.util.stream.Collectors):
toList():转为 List →collect(Collectors.toList())toSet():转为 Set →collect(Collectors.toSet())toMap():转为 Map(需指定 key 和 value) →collect(Collectors.toMap(Student::getName, Student::getScore))joining():字符串拼接(适用于 String 流) →stream.map(Student::getName).collect(Collectors.joining(","))(用逗号分隔)
2. 聚合计算(适用于数值流或对象流)
| 方法 | 作用 | 示例 |
|---|---|---|
count() | 统计元素数量 | long count = stream.count() |
max(Comparator<T> c) | 求最大值 | stream.max(Comparator.comparingInt(Student::getAge)) |
min(Comparator<T> c) | 求最小值 | stream.min(Comparator.comparingDouble(Student::getScore)) |
sum()(数值流) | 求和 | stream.mapToInt(Student::getAge).sum() |
average()(数值流) | 求平均值 | stream.mapToDouble(Student::getScore).average() |
示例:求 18 岁以上学生的最高成绩
Optional<Double> maxScore = students.stream().filter(s -> s.getAge() > 18).map(Student::getScore).max(Double::compare);// Optional处理(避免空指针)
double score = maxScore.orElse(0.0);
3. 匹配判断
| 方法 | 作用 | 示例 |
|---|---|---|
allMatch(Predicate<T> p) | 所有元素是否满足条件 | stream.allMatch(s -> s.getAge() > 18)(是否全是成年人) |
anyMatch(Predicate<T> p) | 是否有元素满足条件 | stream.anyMatch(s -> s.getScore() == 100)(是否有满分) |
noneMatch(Predicate<T> p) | 所有元素是否都不满足条件 | stream.noneMatch(s -> s.getName().isEmpty())(是否没有空姓名) |
示例:检查是否所有学生都及格(假设 60 分为及格线)
boolean allPass = students.stream().allMatch(s -> s.getScore() >= 60);
4. 查找元素
| 方法 | 作用 | 示例 |
|---|---|---|
findFirst() | 返回第一个元素 | stream.findFirst() |
findAny() | 返回任意一个元素(并行流中效率高) | stream.findAny() |
示例:获取第一个成年人学生
Optional<Student> firstAdult = students.stream().filter(s -> s.getAge() > 18).findFirst();
5. 规约(复杂聚合)
| 方法 | 作用 | 示例 |
|---|---|---|
reduce(T identity, BinaryOperator<T> accumulator) | 从初始值开始,将元素累加计算 | 见下方示例 |
示例:计算所有学生的年龄总和(用 reduce 实现)
int totalAge = students.stream().mapToInt(Student::getAge).reduce(0, (a, b) -> a + b); // 0是初始值,a是累加结果,b是当前元素
(三)、并行流相关
当数据量很大时,我们可以轻松将串行流转换为并行流
并行流会自动利用多核处理器进行计算,无需我们手动创建线程池,这对于大数据处理非常有用。
| 方法 | 作用 | 示例 |
|---|---|---|
parallel() | 将串行流转为并行流 | stream.parallel() |
sequential() | 将并行流转为串行流 | stream.sequential() |
示例:大数据量下并行计算平均成绩
// 数据量越大,并行流优势越明显
double avg = students.parallelStream() // 直接获取并行流.mapToDouble(Student::getScore).average().orElse(0.0);四、初学者常见问题
Stream 只能用一次:一个 Stream 执行终止操作后就会关闭,再次使用会报错
Stream<Student> stream = students.stream(); stream.forEach(System.out::println); // 终止操作,流关闭 stream.count(); // 报错:java.lang.IllegalStateException: stream has already been operated upon or closed区分 map 和 flatMap:map 是 "一对一" 转换,flatMap 是 "一对多" 并合并流(如将列表中的列表展开)
Optional 的使用:max ()、findFirst () 等方法返回 Optional,需用 orElse ()、ifPresent () 等方法处理,避免空指针
五、总结
Stream 流的方法虽然多,但核心是围绕 "数据管道" 的思想设计的。记住:先通过中间操作构建处理流程,再用终止操作获取结果。
