5.函数式数据处理-使用流

文章是个人阅读《Java8实战》过程中的重点摘抄,可能晦涩,没有示例代码,后续会补充总结完善。


本章内容

  • 筛选、切片和映射
  • 查找、匹配和归约
  • 使用数值范围等数据流
  • 从多个源创建流
  • 无限流

核心问题

1.StreamAPI常用操作有哪些?
2.常用操作的返回类型、参数类型、函数描述符分别时什么?是中间操作还是终端操作?
3.针对基本类型int、long、double的特化流怎么使用?如何代表数据范围?
4.如何通过其他源来创建流,比如:值、数组、文件、函数
5.无限流的应该如何应用

概述

我们从上一章节中学习到流的概念、流与集合的对比,以及流的简单操作,使用StreamAPI对集合进行迭代的这种方式(内部迭代)是很有用的。因为StreamAPI管理如何处理数据,这样API就可以在背后进行多种优化,比如并行运行我们的代码处理数据。

接下来,我们来学习StreamAPI支持的许多操作。这些操作可以让我们快速完成复杂的数据查询,如筛选、切片、映射、查询、 匹配、归约。他们都是来表达发杂数据的处理。我们还会学习一些特殊的流,数值流、来自文件和数组的多种来源的流,以及无线流。


5.1 筛选与切片

5.1.1 filter() 用谓词筛选

接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包含所有符合谓词元素的流。

List<Dish> vegetarianMenu = menu.stream()
	.filter(Dish::isVegetarian)
	.collect(toList());

5.1.2 distinct() 筛选重复元素

不接受参数,返回一个没有重复元素的流。内部根据流所生成元素的hashCode和equals方法实现。

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
	.filter(i -> i % 2 == 0)
	.distinct()
	.forEach(System.out::println);

5.1.3 limit() 截短流

接受一个int参数作为长度,返回一个不超过给定长度的流。

List<Dish> dishes = menu.stream()
	.filter(d -> d.getCalories() > 300)
	.limit(3)
	.collect(toList());

5.1.4 skip() 跳过元素

接受一个int参数作为扔掉的个数,返回一个扔掉前n个元素的流。如果长度不够会返回一个空流。

List<Dish> dishes = menu.stream()
	.filter(d -> d.getCalories() > 300)
	.skip(2)
	.collect(toList());

5.2 映射

映射涉及到map、flatMap连个方法,flatMap有点不好理解。
这里讲的是映射与转换类似,但是映射是创建一个新的,而不是去修改。

5.2.1 map() 对每个元素应用函数

接受一个函数作为参数,这个函数会被应用到每个元素上,并将其映射为一个新的元素,并返回一个这个新元素的流。

List<String> words = Arrays.asList("Java 8", "Lambdas", "In", "Action");
// length()返回的时Integer,map()返回的时Stream<Integer>,经过toList(),最终返回的List<Integer>
List<Integer> wordLengths = words.stream()
	.map(String::length)
	.collect(toList());

5.2.1 flatMap() 流的扁平化 (不易理解)

使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。简而言之,flatMap方法将把一个流中的每个值换成另一个流,然后将流链接起来,形成新的流。

// 所有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流
List<String> uniqueCharacters = words.stream()
	// 将单词转化为数组
	.map(w -> w.split(""))
	// 将各个生成流扁平化为单个流
	.flatMap(Arrays::stream)
	.distinct()
	.collect(Collectors.toList());

此处用到了Arrays.stream()方法,接受一个数组并产生一个流。

String[] arrayOfWords = {"Goodbye", "World"};
Stream<String> streamOfwords = Arrays.stream(arrayOfWords);

5.3 查找和匹配

查找涉及到allMatch、 anyMatch、 noneMatch这三个方法。都返回boolean,因此都是终端操作。并且他们都是短路求值,对于流而言,某些操作不需要处理整个流就能够得到结果,只要找到一个元素就返回结果。

匹配涉及到findFirst、findAny两个方法。

5.3.1 anyMatch() 检查谓词是否至少匹配一个元素

接受一个函数,返回一个boolean值,是一个终端操作。

if(menu.stream().anyMatch(Dish::isVegetarian)){
	System.out.println("The menu is (somewhat) vegetarian friendly!!");
}

5.3.2 allMatch() 检查谓词是否匹配所有元素

他与anyMatch方法类似。

boolean isHealthy = menu.stream()
	.allMatch(d -> d.getCalories() < 1000);

5.3.3 noneMatch() 检查没有任何元素与谓词匹配

他与allMatch方法表达意义正好相反。

boolean isHealthy = menu.stream()
	.noneMatch(d -> d.getCalories() >= 1000);

5.3.4 findAny() 查找元素

返回当前流中的任意元素,它可以与其他操作流结合使用。

Optional<Dish> dish = menu.stream()
	.filter(Dish::isVegetarian)
	.findAny();

注意:Optional<T>类( java.util.Optional)是一个容器类,代表一个值存在或不存在。Java 8的库设计人员引入了Optional<T>,这
样就不用返回众所周知容易出问题的null了。

5.3.5 findFirst() 查找第一个元素


5.4 归约:reduce()

在此小节中,我们会看到如何把一个流中的元素组合起来,使用reduce操作来表达更复杂的查询。

5.4.1 元素求和

reduce接受两个参数。一个初始值;一个BinaryOperator<T>,将两个元素结合起来形成一个新值。

// for-each
int sum = 0;
for (int x : numbers) {
	sum += x;
}
// reduce
// Lambda反复结合每个元素,直到流被归约成一个值
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
// 通过Integer的sum静态方法来求和。
int sum = numbers.stream().reduce(0, Integer::sum);

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

为什么它返回一个Optional<Integer>呢?考虑流中没有任何元素的情况。reduce操作无法返回其和,因为它没有初始值。这就是为什么结果被包裹在一个Optional对象里,以表明和可能不存在。
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));

5.4.2 最大值和最小值

Optional<Integer> max = numbers.stream().reduce((x, y) -> x < y ? x : y);
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);

归约方式的优势与并行化

相比逐步迭代求和,使用reduce的好处在于,迭代被内部迭代抽象掉了,这样内部实现得以选择并行执行reduce操作。

但要并行执行的代码也要付出一定的代价:传递给reduce的Lambda不能更改状态(如实例变量),而且操作必须满足结合律才可以按任意顺序执行。???

流操作的有状态和无状态

你已经看到了很多的流操作。乍一看流操作简直是灵丹妙药,而且只要在从集合生成流的时侯把Stream换成parallelStream就可以实现并行。

当然,对于许多应用来说确实是这样,就像前面的那些例子。你可以把一张菜单变成流,用filter选出某一类的菜肴,然后对得到的流做map来对卡路里求和,最后reduce得到菜单的总卡路里量。这个流计算甚至可以并行进行。但这些操作的特性并不相同。它们需要操作的内部状态还是有些问题的。

诸如map或filter等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作一般都是无状态 的:它们没有内部状态(假设用户提供的Lambda或方法引用没有内部可变状态)。

但诸如reduce、 sum、 max等操作需要内部状态来累积结果。在上面的情况下,内部状态很小。在我们的例子里就是一个int或double。不管流中有多少元素要处理,内部状态都是有界的。

相反,诸如sort或distinct等操作一开始都和filter和map差不多——都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应ॆ返回最大的质数,但数学告诉我们它不存在)。我们把这些操作作叫作有状态操作

当前已经学习的操作

操作类型返回类型使用的类型/函数式接口(参数)函数描述符
filter中间操作Stream<T>Predicate<T>T -> boolean
distinct中间操作(有状态-无界)Stream<T>
skip中间操作(有状态-无界)Stream<T>Long
limit中间操作(有状态-无界)Stream<T>Long
map中间操作Stream<R>Function<T, R>T -> R
flatMap中间操作Stream<T>Function<T,Stream<R>>T -> Stream<R>
sorted中间操作(有状态-无界)Stream<T>Comparator<T>(T,T) -> int
anyMatch终端booleanPredicate<T>T -> boolean
noneMatch终端booleanPredicate<T>T -> boolean
allMatch终端booleanPredicate<T>T -> boolean
findAny终端Optional<T>
findFirst终端Optional<T>
reduce终端(有状态-有界)Optional<T>BinaryOperator<T>(T,T) -> T
collect终端RConsumer<T,A,R>
foreach终端voidConsumer<T>T -> void
count终端long

5.5 实践

》》》


5.6 数据流

数据流是为了解决在处理int、long、double类型数据的时候,暗含的装箱问题。就是int和Integer之间的效率差异。

StreamAPI中提供了原始流的特化,专门支持处理数据流的方法。

5.6.1 原始类型流特化:IntStream()、DoubleStream()、LongStream()

java8引入了三个原始类型特化流来解决暗含拆箱装箱的问题,IntStream、DoubleStream、LongStream,分别将流中的元素特化为int、double、long。每个接口都带来了常用数值规约的新方法,例如sum、max。此外还包含将特化流转换回对象流的方法。

映射到数值流

将流转换为特化版本的常用方法是mapToInt、 mapToDouble和mapToLong。这些方法和之前的map方法的工作原理一样,只是返回结果是特化流,而不是Stream<T>。

例如:对菜肴的卡路里求和。我们通过对比方式来看下。使用reduce方法进行归约求和的过程中,存在Integer拆箱为int在求和。使用mapToInt,返回的是IntStream而不是Stream<Integer>,然后调用IntStream接口定义的sum方法,求和。注意,流是空的sum默认返回0。

// 非数值流
int calories = menu.stream()
	.map(Dish::getCalories)
	.reduce(0, Integer::sum);
// 数值流
int calories = menu.stream()
	.mapToInt(Dish::getCalories)
	.sum();

转换回对象流

使用boxed方法,将特化流转换回对象流。

IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();

默认值OptionalInt

如何区分没有元素的流和最大值真的是0的流呢,Optional类可以表示存在或不存在的容器。对于三种特化流,也分别有一个Optional特化版本:OptionalInt、OptionalDouble、OptionalLong。

OptionalInt maxCalories = menu.stream()
	.mapToInt(Dish::getCalories)
	.max();
// 如果没有最大值,显示给与最大值
int max = maxCalories.orElse(1);

5.6.2 数值范围:range()、rangeClosed()

两个方法都是接受两个参数,第一个参数接受起始值,第二个参数接受结束值。range不包括结束值,rangeClosed包括结束值。

// 表示[1,100]之间的偶数流
IntStream evenNumbers = IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0);
// 结果是50
System.out.println(evenNumbers.count());

5.6.3 数值流应用

》》》


5.7 构建流

目前为止,流对于表达数据处理查询是非常强大而有用的。我们已经能够使用stream()从集合生成流,还介绍了如何根据数值范围创建数值流。我们将介绍如何从数组、值序列、文件来创建流,还有无限流。

5.7.1 由值创建流

使用静态方法Stream.of,通过显式值创建一个流,他可以接受任意数量的参数。
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
使用静态方法Stream.empty,创建一个空流
Stream<String> emptyStream = Stream.empty();

5.7.2 由数组创建流

使用静态方法Arrays.stream从数组创建一个流,他接受一个数组作为参数。

int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();

5.7.3 由文件生成流

NIO API中很多静态方法可以返回一个流。

使用Files.lines,返回一个由指定文件中的各行构成的字符串流。

// 计算文件中多少个不用的单词
long uniqueWords = 0;
try(Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){
	uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" "))).distinct().count();
}catch(IOException e){
}

5.7.4 由函数生成流:创建无限流

StreamAPI通过两个静态方法来从函数生成流:Stream.iterate和Stream.generate。

注意:无限流需要使用limit进行截短。

迭代:Stream.iterate

此方法接受一个初始值,和一个依次应用在每个产生的新值的Lambda表达式(UnaryOperator<T>)。

Stream.iterate(0, n -> n + 2)
	.limit(10)
	.forEach(System.out::println);

如示例中,iterate操作是顺序的,并且结果取决于前一次的应用。这个操作将生成一个无限流(没有结尾),因为值是按需计算的,可以永远的计算下去。我们也说这个流是无界的。正如我们之前讲流与集合的区别,有无界限是他们之间的关键区别之一。

生成:Stream.generate

方法接受一个Lambda表达式(Supplier<T>)。

Stream.generate(Math::random)
	.limit(5)
	.forEach(System.out::println);

5.8 小节

  • StreamAPI可以表达复杂的数据处理查询,常见操作
  • 流的基本操作,这些都是短路操作,找到结果就立即停止计算。
    • 筛选:filter、distinct
    • 切片:skip、limit
    • 映射(提取和转换流中元素):map、flatMap
    • 查找元素:findFirst、findAny
    • 匹配给定谓词:allMatch、noneMatch、anyMatch
    • 规约(求和、最大值):reduce (非短路)
  • 流有三种基本的原始类型特化:IntStream、DoubleStream、LongStream。
  • 流不仅可以从集合创建,也可以从值、数组、文件、函数(iterate与generate)创建。
  • 无限流是没有固定大小的流。
  • 流的状态:filter和map等操作是无状态的,他们并不存储任何状态。reduce等操作需要存储状态才能计算值。sorted和distinct等操作也需要存储状态,因为他们需要把流中的所有元素缓存起来,才能返回一个新流。这种操作被称为有状态操作。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值