4. Java8新特性-什么是流(Stream)

x33g5p2x  于2021-08-25 转载在 Java  
字(4.7k)|赞(0)|评价(0)|浏览(142)
本节重点内容
  1. 什么是流
  2. 集合与流
  3. 内部迭代与外部迭代
  4. 中间操作与终端操作
流是什么?

流是Java API的新成员,它允许你以声明式的方式处理数据集合(通过查询语句表达,而不是现写一个实现),可以把它看作遍历数据集的高级迭代器。
另外,流还可以透明的并行处理,无须写多余的多线程代码。

先看一段示例,以比较传统方法和流处理集合的不同:

// 现有一批菜肴,需要筛选出卡路里小于400且按卡路里排序,最后打印菜肴的名称

// java8之前的写法
public static void main(String[] args) {
        List<Dish> menus = new ArrayList<>(List.of(
                new Dish("大虾", 100),
                new Dish("大鱼", 200),
                new Dish("大兔", 300),
                new Dish("大狗", 400),
                new Dish("大猪", 500)
        ));


        // java8前写法
        List<String> dishNames = processOld(menus);
        System.out.println(dishNames);
}

private static List<String> processOld(List<Dish> menu) {
     List<Dish> lowCaloricDishes = new ArrayList<>();

     // 先遍历筛选出低于400卡路里的菜
     for (Dish d : menu) {
         if (d.getCalories() < 400) {
             lowCaloricDishes.add(d);
         }
     }

     // 按卡路里排序
     Collections.sort(lowCaloricDishes, new Comparator<>() {
         public int compare(Dish d1, Dish d2) {
             return Integer.compare(d1.getCalories(), d2.getCalories());
         }
     });
     
     // 最后收集菜肴名称
     List<String> lowCaloricDishesName = new ArrayList<>();
     for (Dish dish : lowCaloricDishes) {
         lowCaloricDishesName.add(dish.getName());
     }

     return lowCaloricDishesName;
 }

java8使用流的写法

public static void main(String[] args) {
        List<Dish> menus = new ArrayList<>(List.of(
                new Dish("大虾", 100),
                new Dish("大鱼", 200),
                new Dish("大兔", 300),
                new Dish("大狗", 400),
                new Dish("大猪", 500)
        ));

        // stream写法
        List<String> dishNames = menus.stream().filter(e -> e.getCalories() < 400).sorted(comparing(Dish::getCalories)).map(Dish::getName).collect(Collectors.toList());

        System.out.println(dishNames);
    }

采用stream处理的好处

  1. 代码以声明式的方式写的:说明想要完成什么,而不是说明如何实现(通过for, if等)
  2. 可以把几个基础操作链接起来,来表达复杂的数据处理流水线,同时保持代码清晰可读。

在这里插入图片描述

前人的尝试

在没有StreamAPI之前,很多厂商和个人为了解决繁琐的集合操作,已经做了诸多努力。比如Guava就是Google创建的一个非常流行的库。久远一点的Apache Commons Collections库也提供类似的功能。

如今通过Java8的StreamAPI可写出声明式,可复合,可并行的代码。

流的简介

流的定义:从支持数据处理操作的源生成的元素序列
元素序列:可以访问特定元素类型的一组有序值
:流会使用一个提供数据的源,如:集合,数组,输入/输出资源。注意:从有序集合生成的流会保持原有的顺序
数据处理操作:流支持的操作类似于数据库的操作,以及函数式编程语言中的常用操作,如:filter, map, reduce, find, match, sort等。流操作可以顺序执行,也可以并行执行。
流水线:很多流操作本身会返回一个流,这样可以链式操作形成一个大的流水线。
内部迭代:自己写for-each为显式迭代,自行定义具体步骤,而流的迭代是在背后进行的,所以称为内部迭代。

以下仍以菜肴为示例进行说明:

menus.stream() // 从集合生成流
   .filter(e -> e.getCalories() < 400) 
   .sorted(comparing(Dish::getCalories))
   .map(Dish::getName)
   .limit(2)
   .collect(Collectors.toList());

	// filter, sorted, map, limit都是流操作,本身又返回另一个流,形成一个流水线
	// collect处理流水线,将结果返回,本例中返回一个集合

	// 注意:在调用collect前,没有任何结果产生,根本未实际执行,都在排队等待,直到collect的到来。

在这里插入图片描述

流与集合

集合和流的区别:
它们的差异在于什么时候进行计算,集合是内存中的数据结构,它包含数据结构中目前所有的值-集合中的每个元素都是先计算出来才能添加到集合中。
而流元素则是按需计算的,像是一个延迟创建的集合,只有在有需要时才创建值。

只能遍历一次

和迭代器类似,流只能遍历一次。一次结束了这个流就被消费了,可以从原始源重新获取一个流(前提是可重复的源,如果IO通道就不行了)。

记住:流只能被消费一次

public class DishStreamOnceSample {

    public static void main(String[] args) {
        List<Dish> menus = new ArrayList<>(List.of(
                new Dish("大虾", 100),
                new Dish("大鱼", 200),
                new Dish("大兔", 300),
                new Dish("大狗", 400),
                new Dish("大猪", 500)
        ));

        // 演示流只能消费一次
        Stream<Dish> stream = menus.stream();
        stream.forEach(System.out::println);
        stream.forEach(System.out::println);

    }
}

// result:
Dish(name=大虾, Calories=100)
Dish(name=大鱼, Calories=200)
Dish(name=大兔, Calories=300)
Dish(name=大狗, Calories=400)
Dish(name=大猪, Calories=500)
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
	at java.base/java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
	at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:658)
	at win.elegentjs.java8.stream.DishStreamOnceSample.main(DishStreamOnceSample.java:28)

Process finished with exit code 1
外部迭代&内部迭代

用户自行使用for-each,这称为外部迭代。而内部迭代是指流内部执行。

内部迭代可以透明的并行处理,或者用更优化的顺序处理。

流操作

java.util.stream.Stream中的Stream接口定义了许多操作。它们可以分为两大类:
1)中间操作:filter, map,limit等
2)终端操作:collect,用来触发流行线执行并关闭流水线

在这里插入图片描述

中间操作

如filter,sorted等中间操作会返回另一个流,可通过链式操作形成一个流水线,但重要的是中间操作不会触发流的执行。StreamAPI会自动优化相关步骤,如自动合并,短路。
示例如下:

public class DishStreamDebugSample {

    public static void main(String[] args) {
        List<Dish> menus = new ArrayList<>(List.of(
                new Dish("大虾", 100),
                new Dish("大鱼", 200),
                new Dish("大兔", 300),
                new Dish("大狗", 400),
                new Dish("大猪", 500)
        ));

        List<String> dishNames = menus.stream().
                filter(e -> {
                    System.out.println(e.getCalories() + " filter");
                    return e.getCalories() < 600;
                })
                .map(e -> {
                    System.out.println(e.getCalories() + " map");
                    return e.getName();
                }).limit(2)
                .collect(Collectors.toList());
        System.out.println(dishNames);
    }

}

// result:
// 100 filter
// 100 map
// 200 filter
// 200 map
// [大虾, 大鱼]

可以看出并非是先过滤所有的集合,且filter和map做了合并,limit实现了短路效果。

终端操作

终端操作从流水线生成结果,其结果可以是不是流的任何值,如List, Integer,甚至void。如:

menus.stream().forEach(System.out::println);

此处的forEach是一个终端操作,它的返回值是void。

使用流

流的使用包括三件事:
1)一个数据源
2)一个中间操作链,形成一条流水线
3)一个终端操作,执行流水线,并生成结果

列举几个中间操作和终端操作(部分,不全)

在这里插入图片描述

小结

本节初步学习了什么是流,了解了以下重要概念:
1)流是从支持数据处理操作的源生成的一组元素
2)流利用内部迭代
3)流操作有两类:中间操作和终端操作
4)多个中间操作可串联出流水线,但不会生成任何结果
5)forEach和count等终端操作触发流水线执行,并返回一个非流的值
6)流中的元素计算按需进行

相关文章