《Java 8 in Action》【04】----流概述

文章目录

          • 1.前言
          • 2.流的概念
          • 3.流与集合
          • 4.流操作
          • 5.总结

1.前言

  流是Java8引入的新概念,它允许以声明性方式处理数据集合,可以将其看成遍历数据集的高级迭代器。流还可以透明地并行处理数据,无需写任何多线程代码。可以通过一个简单例子预览下使用流的好处,例如从菜单中筛选出低热量的菜肴名称,并按照卡路里排序,用Java7实现代码如下:

List<Dish> lowCaloricDishes = new ArrayList<>();
for (Dish d : menu) {
     
     if (d.getCalories() < 400) {
     //筛选元素
         lowCaloricDishes.add(d);
     }
 }
 //用匿名类对菜肴排序
 Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
     
     public int compare(Dish d1, Dish d2) {
     
         return Integer.compare(d1.getCalories(), d2.getCalories());
     }
 });
 //处理排序后的菜名列表
 List<String> lowCaloricDishesName = new ArrayList<>();
 for (Dish d : lowCaloricDishes) {
     
     lowCaloricDishesName.add(d.getName());
 }

  这段代码代码中使用了一个"垃圾变量"lowCaloricDishes,它唯一作用是作为一次性的中间容器。Java8将实现细节放在了它本该归属库里,代码实现如下。

List<String> lowCaloricDishesName = menu.stream()
            .filter(dish -> dish.getCalories() < 400)   //筛选
            .sorted(comparing(Dish::getCalories)) //排序
            .map(Dish::getName)     //提取菜名
            .collect(Collectors.toList());

  为了利用多核架构并行执行这段代码,只需要将stream() 换成 parallelStream() 即可。使用这种新的方式解决问题有几个好处:

  1. 首先代码以声明性方式写的,说明了想要完成什么(筛选热量低的菜肴)而不是如何实现一个操作(如利用循环和if条件等控制流语句)。
  2. 其次可以将几个基础操作链接起来,来表达复杂的数据处理流水线,同时保持代码清晰可读,如上面将filter结果传给了sorted方法,再传给map方法,最后传给collect方法。因为filtersortedmapcollect等操作是与具体线程模型无关的高层次构件,所以它们内部可以是单线程的,也可以透明地充分利用多核架构。Stream API可以让数据进行并行处理任务,开发人员无需关心线程和锁了。

  现在可以简单总结一下使用Java8中Stream API编写出的代码:(1)声明性——更简洁,更易读;(2)可复合——更灵活;(3)可并行——性能更好。

2.流的概念

  Java8中提供了很多方式得到流,其中集合里面的stream方法返回一个流(接口定义在java.util.stream.Stream),可以将流简短定义为"从支持数据处理操作的源生成的元素序列"。

  • 元素序列:像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,它的主要目的是以特定的时间/空间复杂度存储和访问元素(如ArrayList)。但流的是目的在于表达计算,比如前面看到的filtersortedmap。简言之,集合讲的是数据,流讲的是计算。
  • :流会使用一个提供数据的源,如集合,数组等。从有序集合生成流时会保留原有的顺序,由列表生成的流,元素顺序与列表一致。

此外流有两个重要的特点。

  1. 流水线:很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。
  2. 内部迭代:与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。

如下面例子体现了上面这些概念。

 List<String> threeHighCaloricDishNames = menu.stream()
            .filter(dish -> dish.getCalories() > 300)//筛选
            .map(Dish::getName)//获取菜名
            .limit(3)//只选择前面三个
            .collect(Collectors.toList());//将结果保存在另外一个List中

  本例中首先对menu菜单调用stream方法得到一个流。数据源就是菜单menu,它给流提供了一个元素序列。接下来对流应用一系列数据处理操作:fitlermaplimitcollect。其中除了collect外,其他操作都会返回另外一个流,这样它们可以接成一条流水线,将这些看作对源的一个查询。最后collect开始操作处理流水线,并返回结果(collect与其他操作不一样,它返回的不是流,此处是一个List)。实际在调用collect之前,没有任何结果产生,也就是没有从menu中选择元素。可以理解为链中的方法调用都在排队等待,直到调用collect。如下图:
《Java 8 in Action》【04】----流概述_第1张图片
  其中

  • filter——接收Lambda,从流中筛选出某些元素。本例中通过传递Lambda d->d.getCalories()>300,选择了热量超过300卡路里的菜肴。
  • map——接收Lambda,将元素转换成其他形式或提取信息,本例中通过传递Dish::getName,相当于Lambda d->d.getName(),提取了每道菜的菜名。
  • limit——截断流,使其元素不超过给定数量。
  • collect——将流转换为其他形式,本例中流被转换为一个List。

 回看这段代码,与Java8之前逐项处理菜单列表的代码大不同。首先通过声明性的方式来处理菜单数据,强调的是对这些数据做什么:“查找热量最高的三道菜名”。我们并没有自己去实现筛选(filter)、提取(map)或者截断(limit)功能,Stream库已经自带了。因此Stream API在决定如何优化这条流水线时更为灵活。例如筛选、提取和截断操作可以一次进行,并在找到这三道菜后立即停止。

3.流与集合

  现有的Java集合概念和流概念都为表示元素类型的值序列集的数据结构提供了接口。所谓有序,就是按顺序取用值,而非随意访问。集合与流最大区别在于什么时候进行计算。集合是一个内存中的数据结构,它包含了数据结构中目前所有的值,集合中的每个元素都得算出来(如添加和删除元素)才能添加到集合中。而流则是概念上的固定的数据结构(不能添加或者删除元素),其元素是按需计算的,用户不可见。

————只能遍历一次
  需要注意的是流只能遍历一次。遍历完之后,流就被消费掉了。可以从原始数据源那里再获得查一次新流来重新遍历一遍,就像迭代器(这里假设它是集合之类的重复源,而非IO通道流)。例如下面的代码会抛出一个异常,因为流已经消费掉了。

List<String> title = Arrays.asList("Java8", "In", "Action");
Stream<String> s = title.stream();
s.forEach(System.out::println);
//java.lang.IllegalStateException:流已被操作或关闭
s.forEach(System.out::println);

————外部迭代与内部迭代
  使用Collection接口需要用户去做迭代(如for-each),这称为外部迭代。相反Stream库使用了内部迭代,因为迭代是由它完成,并负责将结果存储在某个地方,我们只需要提供一个函数来说明要执行的操作。以下代码说明了这种区别:

//1.使用for-each迭代,隐藏了迭代中的一些复杂性,for-each结构是一个语法糖。
List<String> names = new ArrayList<>();
for(Dish d: menu){
     
	names.add(d.getName());
}
//2.使用迭代器迭代
List<String> names = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
while(iterator.hasNext()) {
     
	Dish d = iterator.next();
	names.add(d.getName());
}
//3.流内部迭代
List<String> names = menu.stream()
	.map(Dish::getName)
	.collect(Collectors.toList());

  如下图说明了流(内部迭代)和集合(外部迭代)的区别。
《Java 8 in Action》【04】----流概述_第2张图片

4.流操作

java.util.stream.Stream中的Stream接口定义了许多操作。例如前面提到的例子。

List<String> names = menu.stream() //从菜单获得流
	.filter(d -> d.getCalories() > 300)  //中间操作
	.map(Dish::getName)   //中间操作
	.limit(3)   //中间操作
	.collect(Collectors.toList()); //将Stream转换为 List

  这段代码包含两类操作:

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

  可以连接起来的流操作称为中间操作,诸如filtersorted等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理。中间操作一般都可以合并起来,在终端操作时一次性全部处理。
  关闭流的操作称为终端操作。终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如List、Integer,甚至void 。如forEach是一个返回void的终端操作,它会对源中的每道菜应用一个Lambda。

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

《Java 8 in Action》【04】----流概述_第3张图片
  现在可以总结一下,流的是使用一般包括三件事:

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

  流的流水线背后的理念类似于构建器模式。在构建器模式中有一个调用链用来设置一套配置(对流来说这就是一个中间操作链),接着是调用 built 方法(对流来说就是终端操作)。如下表中简单总结下平常所见的中间流操作和终端流操作。
《Java 8 in Action》【04】----流概述_第4张图片

5.总结
  1. 流是“从支持数据处理操作的源生成的一系列元素”。
  2. 流利用内部迭代:迭代通过 filtermapsorted 等操作被抽象掉了。
  3. 流操作有两类:中间操作和终端操作。如filtermap 等中间操作会返回一个流,并可以链接在一起。可以用它们来设置一条流水线,但并不会生成任何结果。forEachcount等终端操作会返回一个非流的值,并处理流水线以返回结果。
  4. 流中的元素是按需计算的。

你可能感兴趣的:(Java,8,java8)