4.4 流操作

java.util.stream.Stream中的Stream接口定义了许多操作。它们可以分为两大类。再来看一下前面的例子:

  1. List<String> names = menu.stream() ←---- 从菜单获得流
  2. .filter(dish -> dish.getCalories() > 300) ←---- 中间操作
  3. .map(Dish::getName) ←---- 中间操作
  4. .limit(3) ←---- 中间操作
  5. .collect(toList()); ←---- Stream转换为List

你可以看到两类操作:

  • filtermaplimit可以连成一条流水线;
  • collect触发流水线执行并关闭它。

可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。图4-5中展示了这两类操作。这种区分有什么意义呢?

4.4 流操作 - 图1

图 4-5 中间操作与终端操作

4.4.1 中间操作

诸如filtersorted等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理——它们很懒。这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。

为了搞清楚流水线中到底发生了什么,我们把代码改一改,让每个Lambda都打印出当前处理的菜肴(就像很多演示和调试技巧一样,这种编程风格要是搁在生产代码里那就吓死人了,但是学习的时候可以直接看清楚求值的顺序):

  1. List<String> names =
  2. menu.stream()
  3. .filter(dish -> {
  4. System.out.println("filtering:" + dish.getName());
  5. return dish.getCalories() > 300;
  6. }) ←---- 打印当前筛选的菜肴
  7. .map(dish -> {
  8. System.out.println("mapping:" + dish.getName());
  9. return dish.getName();
  10. }) ←---- 提取菜名时打印出来
  11. .limit(3)
  12. .collect(toList());
  13. System.out.println(names);

此代码执行时将打印:

  1. filtering:pork
  2. mapping:pork
  3. filtering:beef
  4. mapping:beef
  5. filtering:chicken
  6. mapping:chicken
  7. [pork, beef, chicken]

你会发现,有好几种优化利用了流的延迟性质。第一,尽管很多菜的热量都高于300卡路里,但只选出了前三个!这是因为limit操作和一种称为短路的技巧,下一章会对此做详细解释。第二,尽管filtermap是两个独立的操作,但它们合并到同一次遍历中了(我们把这种技术叫作循环合并)。

4.4.2 终端操作

终端操作会从流的流水线生成结果,其结果是任何不是流的值,比如ListInteger,甚至void。例如,在下面的流水线中,forEach是一个返回void的终端操作,它会对源中的每道菜应用一个Lambda。把System.out.println传递给forEach,并要求它打印出由menu生成的流中的每一个Dish

  1. menu.stream().forEach(System.out::println);

为了检验你对中间操作和终端操作的理解程度,试试测验4.2吧。

测验4.2:中间操作与终端操作

在下列流水线中,你能找出中间操作和终端操作吗?

  1. long count = menu.stream()
  2. .filter(dish -> dish.getCalories() > 300)
  3. .distinct()
  4. .limit(3)
  5. .count();

答案:流水线中最后一个操作count返回一个long,这是一个非Stream的值。因此它是一个终端操作。所有前面的操作,filterdistinctlimit,都是连接起来的,并返回一个Stream,因此它们是中间操作

4.4.3 使用流

总而言之,流的使用一般包括三件事:

  • 一个数据源(如集合)来执行一个查询;
  • 一个中间操作链,形成一条流的流水线;
  • 一个终端操作,执行流水线,并能生成结果。

流的流水线背后的理念类似于构建器模式。在构建器模式中有一个调用链用来设置一套配置(对流来说这就是一个中间操作链),接着是调用build方法(对流来说就是终端操作)。

为方便起见,表4-1和表4-2总结了你前面在代码例子中看到的中间流操作和终端流操作。请注意这并不能涵盖Stream API提供的操作,你在下一章中还会看到更多。

表 4-1 中间操作

操作 类型 返回类型 操作参数 函数描述符
filter 中间 Stream Predicate T -> boolean
map 中间 Stream Function T -> R
limit 中间 Stream
sorted 中间 Stream Comparator (T, T) -> int
distinct 中间 Stream

表 4-2 终端操作

操作 类型 返回类型 目的
forEach 终端 void 消费流中的每个元素并对其应用Lambda
count 终端 long 返回流中元素的个数
collect 终端 (generic) 把流归约成一个集合,比如ListMap,甚至是Integer。详见第6章