像水一样吧,朋友。—— 李小龙
当我们谈论“流”时,我们在谈论什么
流普及的背景
流式编程(Stream API)是一种有别于传统的面向对象编程的思想,流的普及主要得益于以下几点因素:
- 多核架构处理器日渐普及: 为了提高程序性能,需要利用多核架构并行处理。传统的并行代码(
synchronized、锁、原子类型等)写法复杂,容易出错且难以调试,需要更加简洁完美的写法,最好能够自动对线程进行分配管理,从而将软件工程师的关注点聚焦在产品需求建模上。 - 对大数据处理需求增加: 集合是 Java 中使用最多的 API,几乎每个 Java 应用程序都会制造和处理集合。在 Java 8 之前,JDK 提供了基本的
Collections接口用来处理集合。随着大数据量的场景日益增多,传统的集合接口已经不足以满足业务场景,处理效率低、代码冗余多、运算速度慢、内存占用大,这些问题用已有的集合接口已经无法解决。 - 函数式编程风格适配: Java 8 引入 Lambda 表达式后,需要一套支持 函数式风格 的集合操作 API,以替代冗长的迭代器(
Iterator)和for循环。
流的核心特征
- 流水线化: 链式调用处理数据,
数据源→中间操作(filter/map)→终端操作(collect/count)。 - 惰性求值: 中间操作
延迟执行,直到触发终端操作才计算。 - 自动并行: 通过
parallel()自动启用并行处理,无需手动管理线程。
流与集合的区别
流(Stream)和集合(Collection)都是用来批量处理数据的技术,尽管功能相似,在大部分场景下可以替换使用,但它们具有本质上的区别,如下表所示。
| 区别 | 集合(Collection) | 流(Stream) |
|---|---|---|
| 是否存储数据 | 存储所有数据 | 不存储数据 |
| 迭代方式 | 外部迭代(手动循环) | 内部迭代(自动处理元素) |
| 是否可以多次消费 | 支持多次访问 | 单次消费 |
| 执行计算时机 | 立即计算 | 惰性计算 |
| 容量大小 | 有限容量 | 可处理无限数据(如 Stream.generate()) |
流只能被遍历一次
流只是概念上固定的数据结构,无法向其中添加或删除元素,其元素是按需计算的,具备惰性特质。
和迭代器类似,流只能被遍历一次。当遍历完成后,该流就被消费掉了,一旦尝试对其遍历第二次,就会抛出 IllegalStateException。
List<String> title = Arrays.asList("Java8", "In", "Action");
Stream<String> s = title.stream();
s.forEach(System.out::println);
s.forEach(System.out::println); // ===> throws java.lang.IllegalStateException
以哲学的观点,“流”是在时间中分布的一组值,它们正在不断流逝中,某一时刻只可能触及其中的某一个。而“集合”则是空间(计算机内存)中分布的一组值,在某一时间点上全体存在。
“流”在客户端(Android)开发中的应用
Kotlin 提供了与 Stream 相近的 Flow API,因此我们可以在编写 Android 程序时,享受到流式编程带来的各种便利。这种便利主要体现在两个方面。
一是利用流,对大量数据进行函数式处理,流的函数式接口更接近“做什么”而非“如何去做”,也更符合人思考问题的逻辑。
二则是在编写 Android 应用软件时,要考虑数据流、控制流的单向传递,数据流由数据层流向 UI 层,控制流则反之。利用“流”的思想,将应用程序内部状态的流转抽象化。
流的组成元素
“流”的定义是:从支持数据处理操作的源生成的元素序列。在定义中有几个关键性的元素:
- 源: 流一定具备一个用于提供数据的源,例如集合、数组、输入/输出资源。
- 数据处理操作: 类似于数据库的操作,以及函数式编程语言中的常用操作,如
filter、map、reduce、find、match、sort等。 - 元素序列: 流提供了一个接口,类似集合,该接口可以访问一组有序的元素。流的目的在于表达计算,而非数据存储本身。
此外,流还具备如下核心特点:
- 流水线: 对流进行操作后,会返回一个新的流,因此可以将多个操作级联,从而得到一个大的流水线。
- 内部迭代: 流天生支持迭代,而无需像集合那样
for循环。
一个例子:筛选菜品
以下这段代码,实现的需求是“选出前3个热量 >300kcal 的菜肴”。
List<String> threeHighCalorieDishNames =
menu.stream() // ===> 建立流水线
.filter(d -> d.getCalories() > 300) // ===> 过滤得到热量 >300kcal 菜品
.map(Dish::getName) // ===> 获取菜名
.limit(3) // ===> 取头3个
.collect(toList()) // ===> 保留到 List
在 collect 函数调用前,每一步都是一个流,如下图所示。在 collect 函数调用后,则转换为列表。
操作分类:中间、终结
对流的操作可以分为两类:允许无限衔接的中间操作,以及只能调用一次,调用过后流就终止的终端操作。
mindmap
流的操作
中间操作
终端操作
中间操作
中间操作具备如下特性:
- 衔接:可以无限连接。
- 惰性:除非流水线上附加一个终端操作,否则中间操作不会执行。通常它们会被合并起来一次性处理。
- 短路:中间操作可能不会对所有元素执行,例如
limit(3)只会执行前3个元素。 - 循环合并:独立操作(例如
filter和map)有可能被合并到一次遍历中。
终结(终端)操作
终端操作从 Stream 当中取得结果,结果可以是任何非 Stream 的值,例如 List、Integer 乃至 void。
forEach就是一种返回 void 的终结操作,它用于对流当中的元素执行 Lambda 表达式。
总而言之,在使用流时需要三件事:
- 一个 数据源(如集合) 来执行一个查询;
- 一个 中间操作链,形成一条流的流水线;
- 一个 终端操作,执行流水线,并能生成结果。
借助设计思想,可以用 构建器/builder 模式 来理解流的使用。通过中间操作设置流的属性,通过终端操作触发流
build()。
流的操作函数速查
构建流(Stream Sources)
是创建流的源头,分为集合型、非集合型两类。
// 1. 从集合创建(最常用)
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream1 = list.stream(); // 顺序流
Stream<String> stream2 = list.parallelStream(); // 并行流
// 2. 从数组创建
String[] array = {"a", "b", "c"};
Stream<String> stream3 = Arrays.stream(array);
// 3. 静态工厂方法
Stream<String> stream4 = Stream.of("a", "b", "c"); // 显式元素
Stream<Double> stream5 = Stream.generate(Math::random); // 无限流
Stream<Integer> stream6 = Stream.iterate(0, n -> n + 2); // 迭代流 (0,2,4...)
// 4. 特殊流
IntStream intStream = IntStream.range(1, 5); // 1,2,3,4 (整型流)
LongStream timestampStream = Files.lines(Path.of("log.txt")) // 文件流
.map(line -> parseTimestamp(line));
转换流(Intermediate Operations)
它具备惰性、短路的特征,一些常见的转换操作如下:
| 操作 | 示例 | 功能说明 |
|---|---|---|
| 过滤 | .filter(s -> s.length() > 3) | 保留满足条件的元素 |
| 映射 | .map(String::toUpperCase) | 元素一对一转换 |
| 扁平化 | .flatMap(list -> list.stream()) | 将流中集合"拍平"为单个元素流 |
| 去重 | .distinct() | 删除重复元素 |
| 排序 | .sorted(Comparator.reverseOrder()) | 自定义排序规则 |
| 截取/跳过 | .limit(10).skip(5) | 取前10元素后跳过前5个 |
| 调试 | .peek(System.out::println) | 查看流经元素(不改动) |
// 组合示例:处理字符串流
Stream<String> transformed = Stream.of("apple", "banana", "cherry")
.filter(s -> s.length() > 5) // 保留长度>5的 ["banana","cherry"]
.map(String::toUpperCase) // 转大写 → ["BANANA","CHERRY"]
.flatMap(s -> Stream.of(s.split(""))) // 拆字母 → ["B","A","N",...]
.distinct() // 去重 → ["B","A","N","C","H","E","R","Y"]
.sorted(); // 字母排序 → ["A","B","C",...]
类似代码的图形化描述如下。
收集流(Terminal Operations)
触发计算并关闭流,生成最终结果。
| 操作 | 示例 | 功能说明 |
|---|---|---|
| 聚合 | count(),max(),min(),average() | 计数、最大、最小、平均值 |
| 匹配 | anyMatch(),allMatch(),nonMatch() | 单个、全部、无匹配,支持短路 |
| 消费 | forEach(),forEachOrdered() | 后者在并行流下保证顺序 |
| 收集器 | 代码片段如下 | 功能强大的 Collectors |
// Collectors 收集流示例
// 转为集合
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
// 转为Map
Map<String, Integer> map = stream.collect(
Collectors.toMap(s -> s, String::length) // key=元素, value=长度
);
// 分组(按字符串长度分组)
Map<Integer, List<String>> groups = stream.collect(
Collectors.groupingBy(String::length)
);
// 分区(按长度>5分区)
Map<Boolean, List<String>> partitions = stream.collect(
Collectors.partitioningBy(s -> s.length() > 5)
);
// 拼接字符串
String joined = stream.collect(Collectors.joining(", "));
// 输出: "apple, banana, cherry"
// 自定义收集(计算总和)
Integer sum = stream.collect(
Collectors.reducing(0, String::length, Integer::sum)
);
高级技巧:使用 reduce 对流进行归约
归约(Reduction) 在计算机科学中,特别是在函数式编程和并行计算领域,指的是将数据集中的元素通过某种操作(通常是二元操作)组合起来,生成一个单一的汇总结果的过程。
reduce 用来对流当中的元素进行归约,以整数流 求和操作 为例,其归约过程如下图所示:
流的高级用法
以上对于流的介绍,已经足以解决大多数一般性的需求开发,接下来本节将对流的一些高级用法进行介绍。即使不完全掌握,也无伤大雅。
原始类型流特化
与前一篇文章中介绍的 Lambda 表达式对于基本类型特化相似,在使用流的时候,为了避免自动装箱/拆箱带来的性能损失,JDK 同样提供了基本类型(原始类型)流的特化操作。
Java 8 对于 int、long、double 这三种基本类型,提供了对应的特化流,分别是 IntStream、LongStream 和 DoubleStream。注意这三种特化流并不是对应的 Stream<T>。例如,可以如下文这样,对于团队里所有人的薪资求和:
double salaries = employees.stream() // ===> 返回 Stream<Employee>
.mapToDouble(Employee::getSalary) // ===> 返回 DoubleStream
.sum();
在已有一个特化流的情况下,可以使用 boxed() 函数将其转换为普通流 Stream<T>。
// 将特化流转换回普通流
DoubleStream salaries = employees.stream().mapToDouble(Employee::getSalary);
Stream<Double> stream = salaries.boxed(); // ===> 将数值流转化为 Stream
数值范围
Java 8 引入了名为 range、rangeClosed 的两个静态方法,作用于 IntStream 和 LongStream ,可生成类似 1..100 的常量范围。
它们都接收两个整型参数,其中 range 不包含结束值,而 rangeClosed 则包含结束值。
// 一个从 1 到 100 的偶数流
IntStream evenNumbers = IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0);
参考资料
- Java 8 in Action