《Java8实战》-第四章读书笔记(引入流Stream)

流(Stream)

流是什么

流是Java API的新成员,它容许你以声明性方式处理数据集合(经过查询语句来表达,而不是临时编写一个实现)。就如今来讲,你能够把它们当作遍历数据集的高级迭代器。此外,流还能够透明地并行处理,你无需写任何多线程代码了!我会在后面的笔记中详细记录和解释流和并行化是怎么工做的。咱们简单看看使用流的好处吧。下面两段代码都是用来返回低热量的菜肴名称的,并按照卡路里排序,一个是用Java7写的,另外一个是用Java8的流写的。比较一下。不用太担忧Java 8代码怎么写,咱们在接下来会对它进行详细的了解。java

菜单筛选

使用Java7:git

private static List<String> getLowCaloricDishesNamesInJava7(List<Dish> dishes) {
    List<Dish> lowCaloricDishes = new ArrayList<>();
    // 遍历筛选出低于400卡路里的菜,添加到另一个集合中
    for (Dish d : dishes) {
        if (d.getCalories() < 400) {
            lowCaloricDishes.add(d);
        }
    }

    // 对集合按照卡路里大小进行排序
    List<String> lowCaloricDishesName = new ArrayList<>();
    Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
        @Override
        public int compare(Dish d1, Dish d2) {
            return Integer.compare(d1.getCalories(), d2.getCalories());
        }
    });

    // 遍历将菜名添加到另一个集合中
    for (Dish d : lowCaloricDishes) {
        lowCaloricDishesName.add(d.getName());
    }
    return lowCaloricDishesName;
}
复制代码

在上面的代码中,看起来很冗长,咱们使用了一个“垃圾变量”lowCaloricDishes。它惟一的做用就是做为一次性的中间容器。 在Java8,实现的细节被放到了它本该归属的库力了。 使用Java8:github

private static List<String> getLowCaloricDishesNamesInJava8(List<Dish> dishes) {
    return dishes.stream()
            // 选出400卡路里如下的菜肴
            .filter(d -> d.getCalories() < 400)
            // 按照卡路里排序
            .sorted(comparing(Dish::getCalories))
            // 提取菜名
            .map(Dish::getName)
            // 转为集合
            .collect(toList());
}
复制代码

太酷了!本来十几行的代码,如今只须要一行就能够搞定,这样的感受真的是太棒了!还有一个很棒的新特性,为了利用多核架构并行执行代码,咱们只须要将stream()改成parallelStream()便可:数据库

private static List<String> getLowCaloricDishesNamesInJava8(List<Dish> dishes) {
    return dishes
            .parallelStream()
            // 选出400卡路里如下的菜肴
            .filter(d -> d.getCalories() < 400)
            // 按照卡路里排序
            .sorted(comparing(Dish::getCalories))
            // 提取菜名
            .map(Dish::getName)
            // 转为集合
            .collect(toList());
}
复制代码

你可能会想,在调用parallelStream方法时到底发生了什么。用了多少个线程?对性能有多大的提高?不用着急,在后面的读书笔记中会讨论这些问题。如今,你能够看出,从软件工程师的角度来看,新的方法有几个显而易见的好处。编程

  1. 代码是以声明性的方式写的:说明想要完成什么(筛选热量低的菜肴)而不是说明如何实现一个操做(利用循环和if条件等控制流语句)。
  2. 你能够把几个基础操做连接起来,来表达复杂的数据处理流水线(在 filter 后面接上 sorted 、 map 和 collect 操做),同时保持代码清晰可读。 filter 的结果被传给了 sorted 方法,再传给 map 方法,最后传给 collect 方法。

filter、sorted、map和collect等操做是与具体线程模型无关的高层次构件,因此它们的内部实现能够是单线程的,也可能透明地充分利用你的多核架构!在实践中,这意味着咱们用不着为了让某些数据处理任务并行而去操心线程和锁了,Stream API都替你作好了!数组

如今就来仔细探讨一下怎么使用Stream API。咱们会用流与集合作类比,作点儿铺垫。下一 章会详细讨论能够用来表达复杂数据处理查询的流操做。咱们会谈到不少模式,如筛选、切片、 查找、匹配、映射和归约,还会提供不少测验和练习来加深你的理解。接下来,咱们会讨论如何建立和操纵数字流,好比生成一个偶数流,或是勾股数流。最后,咱们会讨论如何从不一样的源(好比文件)建立流。还会讨论如何生成一个具备无穷多元素的流,这用集合确定是搞不定。bash

流简介

要讨论流,咱们首先来谈谈集合,这是最容易上手的方式了。Java8中的集合支持一个新的stream方法,它会返回一个流(接口定义在 java.util.stream.Stream 里)。你在后面会看到,还有不少其余的方法能够获得流,好比利用数值范围或从I/O资源生成流元素。数据结构

那么,流究竟是什么呢?简短的定义就是“从支持数据处理操做的源生成的元素序列”。让咱们一步步剖析这个定义。多线程

  1. 元素序列:就像集合同样,流也提供了一个接口,能够访问特定元素类型的一组有序值。由于集合是数据结构,因此它的主要目的是以特定的时间/空间复杂度存储和访问元素(如ArrayList 与 LinkedList )。但流的目的在于表达计算,好比你前面见到的filter 、 sorted 和 map 。集合讲的是数据,流讲的是计算。
  2. 源:流会使用一个提供数据的源,如集合、数组或输入/输出资源。请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
  3. 数据处理操做:流的数据处理功能支持相似于数据库的操做,以及函数式编程语言中的经常使用操做,如filter、map、reduce、find、match、sort等。流操做能够顺序执行,也可并行执行。

此外,流操做有两个重要的特色。架构

  1. 流水线:不少流操做自己会返回一个流,这样多个操做就能够连接起来,造成一个大的流水线。
  2. 内部迭代:与使用迭代器显式迭代的集合不一样,流的迭代操做是在背后进行的。

让咱们来看一段可以体现全部这些概念的代码:

List<Dish> menu = Dish.MENU;
// 从menu得到流
List<String> threeHighCaloricDishNames = menu.stream()
        // 经过链式操做,筛选出高热量的菜肴
        .filter(d -> d.getCalories() > 300)
        // 获取菜名
        .map(Dish::getName)
        .limit(3)
        .collect(Collectors.toList());
// [pork, beef, chicken]
System.out.println(threeHighCaloricDishNames);
复制代码

看起来很简单,就算不明白也不要紧,咱们来了解了解,刚刚使用到的一些方法:

  1. filter: 接受Lambda,从流中排除某些元素。在刚刚的代码中,经过传递Lambda表达式 d -> d.getCalories() > 300,选择出热量高于300卡路里的菜肴。
  2. map:接受一个Lambda,将元素转换成其余形式或提取信息。在刚刚的代码中,经过传递方法引用Dish::getName,提取了每道菜的菜名。
  3. limit:截断流,使其元素不超过给定的数量。
  4. collect:将流转换为其余形式。在刚刚的代码中,流被转为一个List集合。

在刚刚解释的这段代码,与遍历处理菜单集合的代码有很大的不一样。首先,咱们使用了声明性的方式来处理菜单数据。咱们并无去实现筛选(filter)、提取(map)或截断(limit)功能,Stream库已经自带了。所以,StreamAPI在决定如何优化这条流水线时更为灵活。例如,筛选、提取和截断操做能够一次进行,并在找到这三道菜后当即中止。

流与集合

Java现有的集合概念和新的流概念都提供了接口,来配合表明元素型有序值的数据接口。所谓有序,就是说咱们通常是按顺序取用值,而不是随机取用的。那这二者有什么区别呢?

打个比方说,咱们在看电影的时候,这些视频就是一个流(字节流或帧流),流媒体视频播放器只要提早下载用户观看位置的那几帧就能够了,这样不用等到流中大部分值计算出来。好比,咱们在Youtube上看的视频进度条随便拖动到一个位置,你会发现它很快就开始播放了,不须要将整个视频都加载好,而是加载了一段。若是,不按照这种方式的话,咱们能够想象一下,视频播放器可能没有将整个流做为集合,保存所须要的内存缓冲区——并且要是非得等到最后一帧出现才能开始看,那等待的时间就太长了,早就没耐心看了。

初略地说,集合与流之间的差别就在于何时进行计算。集合是一个内存中的数据结构,它包含数据结构中目前全部的值,集合中的每一个元素都得先算出来才能添加到集合中。

相比之下,流则是在概念上固定的数据结构,其元素则是按需计(懒加载)算的。须要多少就给多少。这是一种生产者与消费者的关系。从另外一个角度来讲,流就像是一个延迟建立的集合:只有在消费者要求的时候才会生成值。与之相反,集合则是急切建立的(就像黄牛囤货同样)。

流只能遍历一次

请注意,和迭代器相似,流只能遍历一次。遍历完以后,咱们就说这个流已经被消费掉了。你能够从原始数据源那里再得到一个新的流来从新遍历一遍,就像迭代器同样(这里假设它是集合之类的可重复的源,若是是I/O通道就没戏了)。例如如下代码会抛出一个异常,说流已被消费掉了:

List<String> names = Arrays.asList("Java8", "Lambdas", "In", "Action");
Stream<String> s = names.stream();
s.forEach(System.out::println);
// 再继续执行一次,则会抛出异常
s.forEach(System.out::println);
复制代码

千万要记住,它只能消费一次!

外部迭代与内部迭代

使用Collection接口须要用用户去作迭代(好比用for-each),这个称为外部迭代。反之,Stream库使用内部迭代,它帮你把迭代作了,还把获得的流值存在了某个地方,你只要给出一个函数说要干什么就能够了。下面的代码说明了这种区别。

集合:使用for-each循环外部迭代:

// 集合:使用for-each循环外部迭代
List<Dish> menu = Dish.MENU;
List<String> names = new ArrayList<>();
for (Dish dish : menu) {
    names.add(dish.getName());
}
复制代码

请注意, for-each 还隐藏了迭代中的一些复杂性。for-each结构是一个语法糖,它背后的东西用Iterator对象表达出来更要丑陋得多。

集合:用背后的迭代器作外部迭代

List<String> names = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
while(iterator.hasNext()) {
    Dish d = iterator.next();
    names.add(d.getName());
}
复制代码

流:内部迭代

List<String> names = menu.stream()
                    .map(Dish::getName)
                    .collect(toList());
复制代码

让咱们用一个比喻来解释一下内部迭代的差别和好处吧!比方说你在和你两岁的儿子说话,但愿他能把玩家收起来。

你:“儿子,咱们把玩家收起来吧。地上还有玩具吗?”
儿子:“有,球。”
你:“好,放进盒子里。还有吗?”
儿子:“有,那是个人飞机。”
你:“好,放进盒子里。还有吗?”
儿子:“有,个人书。”
你:“好,放进盒子里。还有吗?”
儿子:“没了,没有了。”
你:“好,咱们收好啦!”
复制代码

这正是你天天都要对Java集合作的。你外部迭代一个集合,显式地取出每一个项目再加以处理。若是,你对儿子说“把地上的全部玩具都放进盒子里收起来”就行了。内部迭代比较好的缘由有二:第一,儿子能够选择一只手拿飞机,另外一只手拿球第二,他能够决定先拿离盒子最近的那个东西,而后再拿别的。一样的道理,内部迭代时,项目能够透明地并行处理,或者用更优化的顺序进行处理。要是用Java过去的那种外部迭代方法,这些优化都是很困难的。这彷佛有点儿鸡蛋里挑骨头,但这差很少就是Java 8引入流的理由了,Stream库的内部迭代能够自动选择一种适合你硬件的数据表示和并行实现。与此相反,一旦经过写 for-each 而选择了外部迭代,那你基本上就要本身管理全部的并行问题了(本身管理实际上意味着“某个良辰吉日咱们会把它并行化”或“开始了关于任务和 synchronized 的漫长而艰苦的斗争”)。Java8须要一个相似于Collection 却没有迭代器的接口,因而就有了Stream!下面的图说明了流(内部迭代)与集合(外部迭代)之间的差别。

image

咱们已经了解过了集合与流在概念上的差别,特别是利用内部迭代:替你把迭代作了。可是,只有你已经预先定义好了可以隐藏迭代的操做集合。例如filter或map,这个才有用。大多数这类操做都接受Lambda表达式做为参数,所以咱们能够用前面所了解的知识来参数化其行为。

流操做

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

List<String> names = menu.stream()
                // 中间操做
                .filter(d -> d.getCalories() > 300)
                // 中间操做
                .map(Dish::getName)
                // 中间操做
                .limit(3)
                // 将Stream转为List
                .collect(toList());
复制代码

filter、map和limit能够连成一条线,collect触发流水线执行并关闭它。能够连起来的称为中间操做,关闭流的操做能够称为终端操做。

中间操做

诸如filter和sorted等中间操做会返回一个流。让多个操做能够链接起来造成一个查询。重要的是,除非流水线上触发一个终端操做,不然中间操做不会执行任何处理它们懒得很。这就是由于中间操做通常均可以合并起来,在终端操做时一次性所有处理。

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

List<String>  names = menu.stream()
        .filter(d -> {
            System.out.println("filtering:" + d.getName());
            return d.getCalories() > 300;
        })
        .map(dish -> {
            System.out.println("mapping:" + dish.getName());
            return dish.getName();
        })
        .limit(3)
        .collect(toList());
System.out.println(names);
复制代码

执行结果:

filtering:pork
mapping:pork
filtering:beef
mapping:beef
filtering:chicken
mapping:chicken
[pork, beef, chicken]
复制代码

从上面的打印结果,咱们能够发现有好几种优化利用了流的延迟性质。第一,尽管有不少热量都高于300卡路里,可是只会选择前三个!由于limit操做和一种称为短路的技巧,第二,尽管filter和map是两个独立的操做,可是它们合并到同一次便利中了(咱们把这种技术叫作循环合并)。

终端操做

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

menu.stream().forEach(System.out::println);
复制代码

为了检验一下对终端操做已经中间操做的理解,下面咱们一块儿来看看一个例子:

下面哪些是中间操做哪些是终端操做?

long count = menu.stream()
            .filter(d -> d.getCalories() > 300)
            .distinct()
            .limit(3)
            .count();
复制代码

答案:流水线中最后一个操做是count,它会返回一个long,这是一个非Stream的值。所以,它是终端操做。

使用流

总而言之,流的使用通常包括三件事:

  1. 一个数据源(好比集合)来执行查询
  2. 一个中间操做链,造成一条流的流水线
  3. 一个终端操做,执行流水线,并能生成结果。

流的流水线背后的理念相似于构建器模式。 在构建器模式中有一个调用链用来设置一套配置(对流来讲这就是一个中间操做链),接着是调用built方法(对流来讲就是终端操做)。其实,咱们目前所看的Stream的例子用到的方法并非它的所有,还有一些其余的一些操做。

在本章中,咱们所接触到的一些中间操做与终端操做:

中间:

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

终端:

操做 类型 目的
foreach 终端 消费流中的每一个元素并对其应用 Lambda。这一操做返回 void
count 终端 返回流中元素的个数。这一操做返回 long
collect 终端 把流归约成一个集合,好比 List 、 Map 甚至是 Integer

Stream是一个很是好用的一个新特性,它能帮助咱们简化不少冗长的代码,提升咱们代码的可读性。

本章总结

  1. 流是“从支持数据处理操做的源生成的一系列元素”。
  2. 流利用内部迭代:迭代经过filter、map、sorted等操做被抽象掉了。
  3. 流操做有两类:中间操做和终端操做。
  4. filter和map等中间操做会返回一个流,并能够连接在一块儿。能够用它们来设置一条流水线,但并不会生成任何结果。
  5. forEach和count等终端操做会返回一个非流的值,并处理流水线以返回结果。 6.流中的元素是按需计算(懒加载)的。

代码示例

Github: chap4

Gitee: chap4

公众号

若是,你对Java8中的新特性很感兴趣,你能够关注个人公众号或者当前的技术社区的帐号,利用空闲的时间看看个人笔记,很是感谢!

相关文章
相关标签/搜索