用流水线代替循环
Stream(流)是一条处理一系列值的流水线。你从一个数据源(通常是集合)出发,把“只保留这些”“转换每一个”“排序”这样的操作串起来,最后把结果收集起来。你不用再写一个带临时列表、里面套着 if 的循环,只需描述你想要什么,让流去完成遍历。
既然你已经学会了 lambda,Stream 就水到渠成了:每个操作都接收一个 lambda(或方法引用),描述对单个元素要做的事。
从上往下读:取出名字,保留长的,转成大写,收集成列表。原来的 names 列表从未被修改:流会产生一个新结果,而不去动数据源。
Stream 的结构
每条流水线都由三部分组成:
- 一个数据源:
list.stream()、Arrays.stream(array)或Stream.of(a, b, c)。 - 零个或多个中间操作:
filter、map、sorted、distinct、limit。每个操作都返回另一个流,所以可以把它们串起来。 - 恰好一个终止操作:
collect、count、forEach、reduce、findFirst。它会启动整个计算并产生一个结果(或一个副作用)。
之所以要区分这几部分,是因为有一条规则一开始会让每个人都感到意外。
中间操作是惰性的
中间操作本身什么都不做。它们只是记录下应该发生什么。只有当终止操作要求一个结果时,流水线才会运行。
运行一下:map 里的 lambda 根本没有执行。加上一个终止操作,整条链就活了过来:
这种惰性不是需要对抗的怪癖:它让流可以合并操作,跳过不需要的工作。但这也意味着,一条没有终止操作的流水线什么都不做,这是一个常见的“为什么什么都没发生?”的 bug。
filter 和 map:保留与转换
大部分工作都由这两个操作完成。filter 接收一个 Predicate(返回 boolean 的 lambda),保留通过检验的元素。map 接收一个 Function,用对每个元素应用它的结果来替换该元素。
注意 map 可以改变元素的类型:这里一个 String 流变成了 Integer 流。String::length 是一个方法引用,是 lambda w -> w.length() 的简写。
终止操作产生结果
把流整理好之后,终止操作会把它变成具体的东西:
常见的终止操作有:collect(收集成列表、集合或 Map)、count、forEach、anyMatch / allMatch / noneMatch、findFirst、min / max 和 reduce。终止操作运行之后,这个流就被消费掉了,你不能再复用它。要一条新的流水线,就再调用一次 list.stream()。
收集结果
collect 搭配 Collectors 是构建结果的主力。最常用的是 Collectors.toList():
Collectors 还提供了 toSet()、joining(...)、groupingBy(...) 和 counting()。在 Java 16 及以上版本,可以用更短的 .toList() 代替 collect(Collectors.toList())(它返回一个不可修改的列表):
List<String> result = names.stream().map(String::toUpperCase).toList();
sorted、distinct 和 limit
这些中间操作在收集之前重新整理流:
不带参数的 sorted() 使用自然顺序;要自定义顺序,就传入一个 Comparator(这里是一个 lambda)。Comparator.reverseOrder() 是降序排序更简洁的写法。
reduce:把流归约成一个值
当你需要把所有元素合并成一个结果时,比如求和、求积、找最长的字符串,reduce 就是通用工具。你给它一个初始值和一个合并两个值的函数:
对于简单的求和与求平均值,专用的流更清楚:nums.stream().mapToInt(Integer::intValue).sum()。只有在没有现成的聚合方法时,才用 reduce。
Stream 不能取代所有循环
Stream 最擅长的是把一个集合转换成某个结果。它并不会自动比循环快,而且当你需要修改外部状态,或者以繁琐的方式提前跳出时,用起来很别扭。一条好规则:如果流水线读起来是一句清楚的话,就用 Stream;如果你需要一个共享计数器或 index,那么用普通的 for 循环就很好,也很直白。
还要记住流只能用一次。对它操作两次会抛出异常:
Stream<String> s = names.stream();
s.forEach(System.out::println);
s.count(); // IllegalStateException: stream has already been operated upon
下一页:Optional
有几个终止操作,比如 findFirst、min、max 以及不带初始值的 reduce,可能什么都找不到,所以它们不返回一个单纯的值,而是返回 Optional。这是 Java 表示“可能有值,也可能没有”的容器,它终于给了你一个比返回 null 更干净的选择。这就是下一页的内容。
常见问题
Java 中的 Stream 是什么?
Stream 是一条处理元素序列的流水线,元素通常来自一个集合,经过 filter、map、sorted 这样的一串操作,最后以 collect 或 count 这样的终止操作结束。它不存储数据,也不修改数据源,它描述的是一次计算。用 list.stream() 创建一个流,然后像读菜谱一样从上往下读这条链。
Java 中如何把 Stream 转换回 List?
用一个终止收集器结束流水线:list.stream().filter(...).collect(Collectors.toList())。在 Java 16 及以上版本,可以用更短的 .toList(),它返回一个不可修改的列表。没有终止操作的话,什么都不会运行,因为 filter 和 map 这样的中间操作是惰性的。
什么时候该用 Stream,而不是 for 循环?
当你要把一个集合转换或过滤成某个结果时,就用 Stream:整条流水线读起来就是一句清楚的话(“取出名字,保留长的,转成大写,收集成列表”)。当你需要修改外部状态、以复杂的方式提前跳出,或者逻辑写成命令式的步骤更简单时,就继续用普通的 for 循环。Stream 追求的是清晰,而不是极致的速度。