Java8实战笔记: 第二部分(4~7章)

  • 第四章介绍了流的概念,并解释它与集合的异同
  • 第五章详细讨论了表达复杂数据处理查询可以使用的流操作。会谈到很多模式,如筛选、切片、查找、匹配、映射和归约
  • 第六章介绍了收集器——Stream API的一个功能,可以让你表达更为复杂的数据处理查询
  • 第七章,了解流为何可以自动并行执行,并利用多核架构的优势

4. 引入流

流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,你无需写任何多线程代码了。**那么,流到底是什么呢?简短的定义就是“从支持数据处理操作的源生成的元素序列”。**下面来剖析流的基本定义:

  • 元素序列——就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。但流的目的在于表达计算。集合讲的是数据,流讲的是计算。
  • 源——流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
  • 数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如 filter 、 map 、 reduce 、 find 、 match 、 sort 等。流操作可以顺序执行,也可并行执行。

流操作还有两个重要的特点

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

操作流程

4.1 流和集合的差异

粗略地说,集合与流之间的差异就在于什么时候进行计算。相比之下,流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。

流只能遍历一次

请注意,和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。你可以从原始数据源那里再获得一个新的流来重新遍历一遍

4.2 流操作

流的使用一般包括三件事

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

5. 使用流

5.1 筛选和切片

  1. filter
  2. limit
  3. skip

5.2 映射

流支持 map 方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”而不是去“修改”)

5.2.1 流的扁平化(常见误区)

给 定 单 词 列 表["Hello","World"] ,你想要返回列表 ["H","e","l", "o","W","r","d"]

words.stream() 
    .map(word -> word.split("")) 
    .distinct() 
    .collect(toList()); 

这个方法的问题在于,传递给 map 方法的Lambda为每个单词返回了一个 String[] ( String列表)。 因 此 , map 返 回 的 流 实 际 上 是 Stream<String[]> 类型的.

用flatMap解决问题

flatmap 方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。

5.3 查找和映射

  1. 检查谓词是否至少匹配一个元素 anyMatch
  2. 检查谓词是否匹配所有元素: 全部满足 allMatch, 全部不满足 noneMatch
  3. 返回当前流中的任意元素 findAny
  4. 查找第一个元素findFirst

5.4 归约

使用 reduce 操作来表达更复杂的查询,比如“计算菜单中的总卡路里”或“菜单中卡路里最高的菜是哪一个”。此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个 Integer 。这样的查询可以被归类为归约操作(将流归约成一个值)。用函数式编程语言的术语来说,这称为折叠(fold),因为你可以将这个操作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果

int sum = numbers.stream().reduce(0, (a, b) -> a + b);

reduce 接受两个参数:

  1. 一个初始值,这里是0
  2. 一个 BinaryOperator<T>来将两个元素结合起来产生一个新值,这里我们用的是lambda (a, b) -> a + b

reduce还有一个重载的变体,它不接受初始值,但是会返回一个 Optional 对象:

  • 求最大值:Optional<Integer> max = numbers.stream().reduce(Integer::max);
    • 写成Lambda (x, y) -> x < y ? y : x 而不是 Integer::max
  • 求最小值:Optional<Integer> min = numbers.stream().reduce(Integer::min);

**流操作:无状态和有状态 **

诸如 map 或 filter 等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作一般都是无状态的:它们没有内部状态。

从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们把这些操作叫作有状态操作

5.5 数值流

int calories = menu.stream() 
                   .map(Dish::getCalories) 
                   .reduce(0, Integer::sum); 

这段代码的问题是,它有一个暗含的装箱成本。每个Integer都必须拆箱成一个原始类型,再进行求和。

Java 8引入了三个原始类型特化流接口来解决这个问题: IntStreamDoubleStreamLongStream,分别将流中的元素特化为intlongdouble,从而避免了暗含的装箱成本。

  1. 映射到数值流

    mapToInt 、 mapToDouble 和 mapToLong

    int calories = menu.stream() 
                       .mapToInt(Dish::getCalories)  
                       .sum(); 
    
  2. 转换回对象流

  3. 默认值OptionalInt

    求和的那个例子很容易,因为它有一个默认值: 0 。但是,如果你要计算 IntStream 中的最大元素,就得换个法子了,因为 0 是错误的结果。

    OptionalInt、OptionalDouble和OptionalLong

5.6 构建流

  • of 工厂:Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");

  • empty 工厂:Stream<String> emptyStream = Stream.empty();

  • 由数组创建流:

    • int[] numbers = {2, 3, 5, 7, 11, 13}; int sum = Arrays.stream(numbers).sum();
  • 由文件创建流:Files.lines ,它会返回一个由指定文件中的各行构成的字符串流

  • 创建无限流: Stream API提供了两个静态方法来从函数生成流: Stream.iterateStream.generate 。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。

6. 用流收集数据

你可以把Java 8的流看作花哨又懒惰的数据集迭代器。它们支持两种类型的操作:中间操作(如 filter 或 map )和终端操作(如 count 、 findFirst 、 forEach 和 reduce )。中间操作可以链接起来,将一个流转换为另一个流。这些操作不会消耗流,其目的是建立一个流水线。与此相反,终端操作会消耗流,以产生一个最终结果。具体的做法是通过定义新的Collector接口来定义的,因此区分CollectionCollectorcollect 是很重要的。

image-20201124112654714

Lambda风格

image-20201124112717113

Collector 接口中方法的实现决定了如何对流执行归约操作,Collectors 实用类提供了很多静态工厂方法。从 Collectors类提供的工厂方法(例如 groupingBy )创建的收集器,它们主要提供了三大功能:

  1. 将流元素归约和汇总为一个值
  2. 元素分组
  3. 元素分区

Collectors是一个实现了Collector接口的实现类,扩展了注入toCollection,toList等许多方法

6.1 归约和汇总

  1. 汇总: long howManyDishes = menu.stream().collect(Collectors.counting());
    1. 或者 long howManyDishes = menu.stream().count();
  2. 最大值/最小值: Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy(dishCaloriesComparator));
  3. 汇总2:int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
    1. 平均值: double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories));
  4. 连接字符串: String shortMenu = menu.stream().map(Dish::getName).collect(joining());

6.2 分组

Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType)); 
输出:{FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza], MEAT=[pork, beef, chicken]} 

//================= 自定义逻辑
public enum CaloricLevel { DIET, NORMAL, FAT } 
 
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect( 
        groupingBy(dish -> { 
               	if (dish.getCalories() <= 400) return CaloricLevel.DIET; 
              	else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL; 
        		else return CaloricLevel.FAT; 
         })); 

image-20201124115620935

6.3 分区

分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到的分组 Map 的键类型是 Boolean ,于是它最多可以分为两组—— true是一组,false 是一组。

image-20201124120011889

6.4 自定义Collectors

自己看书=.= P135

7. 并行数据处理与性能

流是如何在幕后应用Java 7引入的分支/合并框架的?

  • 并行流:parallel()
  • 顺序流:sequential()

配置并行流使用的线程池

并行流内部使用了默认的 ForkJoinPool,它默认的线程数量就是你的处理器数量,这个值是由 Runtime.getRuntime().availableProcessors() 得到的。 但是你可以通过系统属性 java.util.concurrent.ForkJoinPool.common.parallelism 来改变线程池大小,如下所示:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
这是一个全局设置,因此它将影响代码中所有的并行流。反过来说,目前还无法专为某个并行流指定这个值。一般而言,让 ForkJoinPool 的大小等于处理器数量是个不错的默认值,除非你有很好的理由,否则我们强烈建议你不要修改它。

7.1 高效使用并行流

  • 在考虑选择顺序流还是并行流时,第一个也是最重要的建议就是用适当的基准来检查其性能。
  • 留意装箱。自动装箱和拆箱操作会大大降低性能。Java 8中有原始类型流(IntStreamLongStreamDoubleStream)来避免这种操作,但凡有可能都应该用这些流。
  • 有些操作本身在并行流上的性能就比顺序流差。特别是limitfindFirst 等依赖于元素顺序的操作,它们在并行流上执行的代价非常大。例如, findAny 会比 findFirst 性能好,因为它不一定要按顺序来执行。你总是可以调用 unordered 方法来把有序流变成无序流。那么,如果你需要流中的n个元素而不是专门要前n个的话,对无序并行流调用limit 可能会比单个有序流(比如数据源是一个 List )更高效。
  • 还要考虑流的操作流水线的总计算成本。设N是要处理的元素的总数,Q是一个元素通过流水线的大致处理成本,则N*Q就是这个对成本的一个粗略的定性估计。Q值较高就意味着使用并行流时性能好的可能性比较大。
  • 对于较小的数据量,选择并行流几乎从来都不是一个好的决定。
  • 要考虑流背后的数据结构是否易于分解。例如, ArrayList 的拆分效率比 LinkedList高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历。另外,用 range 工厂方法创建的原始类型流也可以快速分解。

7.2 分支/合并框架

**分支/合并框架的目的是以递归方式将可以并行的任务拆分成更小的任务,然后将每个子任务的结果合并起来生成整体结果。**它是 ExecutorService 接口的一个实现,它把子任务分配给线程池(称为 ForkJoinPool )中的工作线程。

7.2.1 使用RecursiveTask

要把任务提交到这个池,必须创建 RecursiveTask<R> 的一个子类,其中 R 是并行化任务(以及所有子任务)产生的结果类型,或者如果任务不返回结果,则是 RecursiveAction 类型(当然它可能会更新其他非局部机构)。要定义 RecursiveTask,只需实现它唯一的抽象方法compute

protected abstract R compute();

核心逻辑

这个方法同时定义了将任务拆分成子任务的逻辑,以及无法再拆分或不方便再拆分时,生成单个子任务结果的逻辑。

if (任务足够小或不可分) { 
    顺序计算该任务  
} else { 
    将任务分成两个子任务 
    递归调用本方法,拆分每个子任务,等待所有子任务完成 
    合并每个子任务的结果 
} 

image-20201125220857192

代码

image-20201125221047924

请注意在实际应用时,使用多个 ForkJoinPool 是没有什么意义的。正是出于这个原因,一般来说把它实例化一次,然后把实例保存在静态字段中,使之成为单例,

7.2.2 最佳做法
  • 对一个任务调用 join 方法会阻塞调用方,直到该任务做出结果。因此,有必要在两个子任务的计算都开始之后再调用它。
  • 不应该在 RecursiveTask 内部使用 ForkJoinPool 的 invoke 方法。相反,你应该始终直接调用 compute 或 fork 方法,只有顺序代码才应该用 invoke 来启动并行计算。
  • 对子任务调用 fork 方法可以把它排进 ForkJoinPool 。同时对左边和右边的子任务调用它似乎很自然,但这样做的效率要比直接对其中一个调用 compute 低。这样做你可以为其中一个子任务重用同一线程,从而避免在线程池中多分配一个任务造成的开销。

7.3 Spliterator

看书,几乎用不上 P155

附录

Stream库常用函数

Collectors类的静态工厂方法

image-20201124120222464

image-20201124120235408

流的数据源和可分解性

image-20201124173038902

练习

  1. 给定两个数字列表,如何返回所有的数对呢?例如,给定列表[1, 2, 3]和列表[3, 4],应该返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]。为简单起见,你可以用有两个元素的数组来代表数对。
/**
你可以使用两个 map 来迭代这两个列表,并生成数对。但这样会返回一个 Stream<Stream<Integer[]>> 。你需要让生成的流扁平化,以得到一个 Stream<Integer[]> 。这正是 flatMap 所做的:
*/
List<Integer> numbers1 = Arrays.asList(1, 2, 3); 
List<Integer> numbers2 = Arrays.asList(3, 4); 
List<int[]> pairs = 
    numbers1.stream() 
            .flatMap(i -> numbers2.stream() 
                                  .map(j -> new int[]{i, j}) 
                    ).collect(toList()); 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值