文章是个人阅读《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 | 终端 | boolean | Predicate<T> | T -> boolean |
noneMatch | 终端 | boolean | Predicate<T> | T -> boolean |
allMatch | 终端 | boolean | Predicate<T> | T -> boolean |
findAny | 终端 | Optional<T> | ||
findFirst | 终端 | Optional<T> | ||
reduce | 终端(有状态-有界) | Optional<T> | BinaryOperator<T> | (T,T) -> T |
collect | 终端 | R | Consumer<T,A,R> | |
foreach | 终端 | void | Consumer<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等操作也需要存储状态,因为他们需要把流中的所有元素缓存起来,才能返回一个新流。这种操作被称为有状态操作。