• 本篇使用的基本的数据说明如下 ```java @Data @AllArgsConstructor public class Dish { // 菜名 private final String name; // 是不是素菜 private final boolean vegetarian; // 卡路里 private final int calories; // 类型 private final Type type;

    public enum Type {

    1. MEAT, FISH, OTHER

    } }

  1. ```java
  2. // Dish客户端,提供一批菜
  3. public class DishClient {
  4. public static final List<Dish> getMenu() {
  5. return Arrays.asList(new Dish("pork", false, 800, Dish.Type.MEAT), new Dish("beef", false, 700, Dish.Type.MEAT), new Dish("chicken", false, 400, Dish.Type.MEAT),
  6. new Dish("french fries", true, 530, Dish.Type.OTHER), new Dish("rice", true, 350, Dish.Type.OTHER), new Dish("season fruit", true, 120, Dish.Type.OTHER),
  7. new Dish("pizza", true, 550, Dish.Type.OTHER), new Dish("prawns", false, 400, Dish.Type.FISH), new Dish("salmon", false, 450, Dish.Type.FISH));
  8. }
  9. }

什么是流

  • 集合是Java中使用最多的API。要是没有集合,还能做什么呢?几乎每个Java应用程序都会制造和处理集合。集合对于很多编程任务来说都是非常基本的:它们可以让你把数据分组并加以处理。为了解释集合是怎么工作的,想象一下你准备列出一系列菜,组成一张菜单,然后再遍历一遍,把每盘菜的热量加起来。你可能想选出那些热量比较低的菜,组成一张健康的特殊菜单。尽管集合对于几乎任何一个Java应用都是不可或缺的,但集合操作却远远算不上完美。
  • 很多业务逻辑都涉及类似于数据库的操作,比如对几道菜按照类别进行分组(比如全素菜肴),或查找出最贵的菜。
  • 要是要处理大量元素又该怎么办呢?为了提高性能,你需要并行处理,并利用多核架构。但写并行代码比用迭代器还要复杂,而且调试起来也够受的!
  • 那Java语言的设计者能做些什么,来帮助你节约宝贵的时间,让你这个程序员活得轻松一点儿呢?答案就是流。
  • 流是什么
  • 流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)
  • 就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,你无需写任何多线程代码了。
  • 流使用案例
  • Java7之前的处理如下

    1. public class Java7Action {
    2. public static void main(String[] args) {
    3. List<Dish> menu = DishClient.getMenu();
    4. List<Dish> lowCaloricDishes = new ArrayList<>();
    5. // 筛选元素
    6. for (Dish dish : menu) {
    7. if (dish.getCalories() < 400) {
    8. lowCaloricDishes.add(dish);
    9. }
    10. }
    11. // 排序
    12. Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
    13. @Override
    14. public int compare(Dish o1, Dish o2) {
    15. return Integer.compare(o1.getCalories(), o2.getCalories());
    16. }
    17. });
    18. // 取名字
    19. List<String> lowCaloricDishNames = new ArrayList<>();
    20. for (Dish lowCaloricDish : lowCaloricDishes) {
    21. lowCaloricDishNames.add(lowCaloricDish.getName());
    22. }
    23. System.out.println(lowCaloricDishNames);
    24. }
    25. }
    26. // [season fruit, rice]
  • 使用Java8流式处理

    1. public class Java8Action {
    2. public static void main(String[] args) {
    3. List<Dish> menu = DishClient.getMenu();
    4. List<String> lowCaloricDishNames = menu.stream().filter(dish -> dish.getCalories() < 400).sorted(((o1, o2) -> Integer.compare(o1.getCalories(), o2.getCalories())))
    5. .map(Dish::getName).collect(Collectors.toList());
    6. System.out.println(lowCaloricDishNames);
    7. }
    8. }
    9. // [season fruit, rice]
  • 上述二者处理的结果是一样的

  • 如果需要并行处理,只需要将stream()换成 parallelStream()

    1. public class Java8Action {
    2. public static void main(String[] args) {
    3. List<Dish> menu = DishClient.getMenu();
    4. List<String> lowCaloricDishNames = menu.parallelStream().filter(dish -> dish.getCalories() < 400).sorted(((o1, o2) -> Integer.compare(o1.getCalories(), o2.getCalories())))
    5. .map(Dish::getName).collect(Collectors.toList());
    6. System.out.println(lowCaloricDishNames);
    7. }
    8. }
  • 你可能会想,在调用parallelStream方法的时候到底发生了什么。用了多少个线程?对性能有多大提升?这些会在后面安排!

  • 现在可以看出来非常显而易见的好处:你目光所在之处,就知道做了什么事情。

    1. ist<String> lowCaloricDishNames = menu.parallelStream(). // 流化
    2. filter(dish -> dish.getCalories() < 400). // 过滤卡路里小于400的
    3. sorted(((o1, o2) -> Integer.compare(o1.getCalories(), o2.getCalories()))). // 根据 卡路里排序
    4. map(Dish::getName). // 获取名字
    5. collect(Collectors.toList()); // 收集为List返回
  • 代码是以声明性方式写的:说明想要完成什么(筛选热量低的菜肴)而不是说明如何实现一个操作(利用循环和if条件等控制流语句)

  • 你可以把几个基础操作链接起来,来表达复杂的数据处理流水线(在filter后面接上sorted、map和collect操作),同时保持代码清晰可读。filter的结果被传给了sorted方法,再传给map方法,最后传给collect方法。

image.png

  • 因为filter、sorted、map和collect等操作是与具体线程模型无关的高层次构件,所以它们的内部实现可以是单线程的,也可能透明地充分利用你的多核架构。
  • 新的Stream API表达能力非常强:读到就知道。
  • 总结一下
  • 声明性:更简洁、更易读
  • 可复合:更灵活
  • 可并行:性能更好
  • 现在就准备进行实操了,来快乐的玩耍。
  • 我们会谈到很多模式,如筛选、切片、查找、匹配、映射和归约,还会提供很多测验和练习来加深你的理解。

  • 要谈论流,就要先谈谈集合。Java集合支持一个新的Stream方法,他会返回一个流(接口定义在java.util.stream.Stream里)

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

    • 流水线:很多流操作本身就会返回一个流,这样多个操作就可以连接起来,形成一个大的流水线。流水线的操作可以看作对数据源进行数据库式查询。
    • 内部迭代:与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。
      1. public class Test1 {
      2. public static void main(String[] args) {
      3. List<String> list = DishClient.getMenu().stream().filter(d -> d.getCalories() > 300).map(Dish::getName).limit(3).collect(Collectors.toList());
      4. System.out.println(list);
      5. }
      6. }
  • 在这个例子中,我们先是menu调用steam方法,由菜单得到一个流。数据源是菜肴列表(菜单),它给流提供一个元素序列。

  • 然后对流进行一系列的操作:filter、map、limit、collect;除了collect之外,所有的这些操作都会返回另外一个流,这样他们就可以构成一条流水线,可以理解为对源的一个查询,最后collect开始处理流水线,并返回结果。在调用collect之前,没有任何结果产生,实际上没有从menu里选择元素,你可以这样理解:链中的方法调用都在排队等待,直到调用collect。下图显示了流操作的的顺序:filter、map、limit、collect。

image.png

  • filter:接受Lambda,从流中排除某些元素。在本例中,通过传递lambdad->d.getCalories() > 300,选择出热量超过300卡路里的菜肴。
  • map:接受一个Lambda,将元素转换成其他形式或提取信息。在本例中,通过传递方法引用Dish::getName,相当于Lambda d-> d.getName(),提取了每道菜的菜名。
  • limit——截断流,使其元素不超过给定数量。
  • collect——将流转换为其他形式。在本例中,流被转换为一个列表。可以把collect看作能够接受各种方案作为参数,并将流中的元素累积成为一个汇总结果的操作。这里的toList()就是将流转换为列表的方案。
  • 注意:我们刚刚解释的这段代码,与逐项处理菜单列表的代码有很大的不同。首先使用声明的方式来处理菜单数据,也就是表达出来你想做什么:查找热量最高的三道菜的菜名。没有实现筛选(filter)、提取(map)、或者截断(limit)的功能。这些功能Stream已经自带了。筛选、提取和截断操作可以一次进行,并在找到这三道菜后立即停止。

    集合与流

  • Java现有的集合概念和新的流概念都提供了接口,来配合代表元素型有序值的数据接口。所谓有序,就是说我们一般是按顺序取用值,而不是随机取用的。

  • 集合与流之间的差异就在于什么时候进行计算:集合是内存中的数据结构,其包含数据结构的所有的值,集合中的每个元素都得先算出来才能添加到集合中,你可以往集合里加东西或者删东西,但是不管什么时候,集合中的每个元素都是放在内存里的,元素都得先算出来才能成为集合的一部分。而流则是在概念上固定的数据结构(不能添加元素和删除元素),其元素是按需计算的。从另外一个角度来说,流是一个延迟创建的集合:只有在消费者要求的时候才会计算值,与此相反,集合则是立即创建的。
  • 流只能遍历一次:其和迭代器一样,只能遍历一次,那么问题就来了,每次迭代都做了什么事情,会不会因此效率变低?这个在后面讲解性能的时候,会进行测试。
  • 当流遍历完之后,这个时候这个流就被销毁了。
  • 集合和流的另一个关键区别在于它们遍历数据的方式。

    外部迭代和内部迭代

  • 使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。

  • 而Streams库使用内部迭代:它把迭代帮我们做了,还把得到的流值存在了某个地方,你只需要给出一个函数说要干什么就可以了。 ```java public class Test2 { public static void main(String[] args) {

    1. List<Dish> menu = DishClient.getMenu();
    2. // 外部迭代
    3. for (Dish dish : menu) {
    4. System.out.println(dish);
    5. }
    6. // 内部迭代
    7. menu.stream().forEach(System.out::println);

    } }

  1. - 举个例子
  2. - 你:把玩具收起来,地上还有玩具吗?
  3. - 我:有,球
  4. - 你:把球放在盒子里,还有吗?
  5. - 我:有,洋娃娃。
  6. - 你:把洋娃娃放在盒子里,还有吗?
  7. - 我:没有了
  8. - 这正是每天需要对集合做的。外部迭代一个集合,取出数据,然后做判断或者处理。
  9. - 其实只需要说“把所有的玩具都收起来就好了”。
  10. - 内部迭代的好处:可以一只手拿球,一只手拿洋娃娃;可以决定先拿哪个最近的东西。
  11. - 同理:内部迭代时,项目可以透明地并行处理,或者用更优化的顺序进行处理。Streams库的内部迭代可以自动选择一种适合你硬件的数据表示和并行实现。
  12. - Java 8需要一个类似于Collection却没有迭代器的接口,于是就有了Stream
  13. - 下图说明了流(内部迭代)和集合(外部迭代)的差异。
  14. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/1806904/1646320199190-1b9f58fc-7337-4c99-a141-808e796ec90b.png#clientId=u10d72aa4-3f0a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=401&id=u718b979e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=802&originWidth=1244&originalType=binary&ratio=1&rotation=0&showTitle=false&size=81437&status=done&style=none&taskId=u3f24ff28-8909-4f73-bfd5-a2762975da7&title=&width=622)
  15. - 集合与流在概念上的差异,特别是流利用了内部迭代:替你把迭代做了
  16. <a name="jlQgk"></a>
  17. # 中间操作和终端操作
  18. - java.util.stream.Stream中的Stream接口定义了许多操作。它们可以分为两大类。我们再来看一下前面的例子
  19. ```java
  20. public class Test1 {
  21. public static void main(String[] args) {
  22. List<String> list = DishClient.getMenu().stream()
  23. .filter(d -> d.getCalories() > 300) // 中间操作
  24. .map(Dish::getName) // 中间操作
  25. .limit(3) // 中间操作
  26. .collect(Collectors.toList()); // 将Stream转换为List
  27. System.out.println(list);
  28. }
  29. }
  • 可以看到两类操作:filter、map和limit可以连成一条流水线;collect触发流水线执行并关闭它。
  • 可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。

image.png

  • 中间操作
  • 诸如filter或sorted等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理——它们很懒。这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。
  • 为了搞清楚流水线中到底发生了什么,我们把代码改一改,让每个Lambda都打印出当前处理的菜肴(就像很多演示和调试技巧一样,这种编程风格要是搁在生产代码里那就吓死人了,但是学习的时候却可以直接看清楚求值的顺序) ```java public class Test1 { public static void main(String[] args) {
    1. List<String> list = DishClient.getMenu() //
    2. .stream()//
    3. .filter(d -> {
    4. System.out.println("filter" + d.getName());
    5. return d.getCalories() > 300;
    6. })//
    7. .map(d -> {
    8. System.out.println("mapping" + d.getName());
    9. return d.getName();
    10. })//
    11. .limit(3)//
    12. .collect(Collectors.toList());
    13. System.out.println(list);
    } }
  1. - 你会发现,有好几种优化利用了流的延迟性质。第一,尽管很多菜的热量都高于300卡路里,但只选出了前三个!这是因为limit操作和一种称为短路的技巧,我们会在下一章中解释。第二,尽管filtermap是两个独立的操作,但它们合并到同一次遍历中了(我们把这种技术叫作循环合并)
  2. - **终端操作**
  3. - 终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如ListInteger,甚至void
  4. ```java
  5. public static void main(String[] args) {
  6. List<Dish> menu = DishClient.getMenu();
  7. // 内部迭代
  8. menu.stream().forEach(System.out::println);
  9. }
  10. }
  • 使用流
  • 总而言之,流的使用一般包括三件事
  • 一个数据源(如集合)来执行一个查询;
  • 一个中间操作链,形成一条流的流水线;
  • 一个终端操作,执行流水线,并能生成结果
  • 流的流水线背后的理念类似于构建器模式。[插图]在构建器模式中有一个调用链用来设置一套配置(对流来说这就是一个中间操作链),接着是调用built方法(对流来说就是终端操作)

    小结

  • 流是“从支持数据处理操作的源生成的一系列元素”。

  • 流利用内部迭代:迭代通过filter、map、sorted等操作被抽象掉了。
  • 流操作有两类:中间操作和终端操作。
    • filter和map等中间操作会返回一个流,并可以链接在一起。可以用它们来设置一条流水线,但并不会生成任何结果。
  • forEach和count等终端操作会返回一个非流的值,并处理流水线以返回结果。
  • 流中的元素是按需计算的。

    参考文章

  • 《Java 8 in Action》

  • 《Java8函数式编程》