Java8 实战读书笔记(4~6章)
第4章 引入流
流是什么?
流 是Java API 的新成员,它允许你以声明性方式处理数据集合。你可以把它们看作是遍历数据集的高级迭代器。
优点:
- 代码是以声明性方式写的:说明想要完成什么而不是说明如何实现一个操作。
- 可以把几个基础操作连接起来,来表达复杂的数据处理流水线。
写出代码的特性:
- 声明性
- 可复合
- 可并行
流简介
流:从支持数据处理操作的源生成的元素序列。
对以上定义中单个词进行剖析:
- 元素序列: 就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。集合讲的是数据,流讲的是计算。
- 源 流会使用一个提供数据的源(也就是数据的来源),从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表保持一致。
- 数据处理操作 流的数据处理功能支持类似于数据库的操作。流操作可以顺序执行也可以并行执行。
此外流操作还有两个重要的特点: - 流水线 很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。
- 内部迭代 与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。
部分操作:
- filter 接受lambda,从流中排序某些元素。
- map 接受一个lambda,将元素转换成其他形式或提取信息。
- limit 截断流,使其元素不超过给定元素。
- collect 将流转换为其他形式。用的最多的就是toList。
流与集合
差异性:
- 执行时段 集合是,元素都得先算出来才能为集合的一部分,而流是按需计算的,像是一个延时创建的集合。
只能遍历一次
流只能消费一次
外部迭代与内部迭代
使用Collection接口需要用户去做迭代(比如for-each),这称为外部迭代。相反Stream库使用内部迭代,它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了。
流操作
流操作分为两大类:
- 中间操作
- 终端操作
中间操作
中间操作会由一个流返回另外一个流,除非流水线上触发一个终端操作否则中间操作不会执行任何操作。
终端操作
终端操作会从流的流水线生成结果,其结果是任何不为流的值。
使用流
总而言之,流的使用一般包括三件事:
- 一个数据源(如集合)来执行一个查询;
- 一个中间操作链,形成一条流水线;
- 一个终端操作,执行流水线,并能生成结果。
注意上面的终端操作,一般在工作过程中我们对一个集合排序,写代码时会发现,有List.sort(),和List.stream.sorted(),如下所示:
示例:
@Test
public void test() {
List list = Arrays.asList(new TestBean2(5, "e"), new TestBean2(2, "d"), new TestBean2(2, "c"), new TestBean2(4, "b"), new TestBean2(1, "a"));
// list排序会有种误解,list.sort()和list.stream.sorted()有什么区别,后者是用的流,如果没有终端操作(collect())这个list不会发生任何变化。
List list1 = list.stream().sorted(Comparator.comparing(TestBean2::getAge).thenComparing(TestBean2::getName)).collect(Collectors.toList());
list1.stream().map(TestBean2::toString).forEach(System.out::println);
}
部分中间操作:
操作 | 类型 | 返回类型 | 操作参数 | 函数描述符 |
---|---|---|---|---|
filter | 中间 | Stream |
Predicate |
T -> boolean |
map | 中间 | Stream |
Function |
T -> R |
limit | 中间 | Stream |
||
sorted | 中间 | Stream |
Compartor |
(T,T) -> int |
distinct | 中间 | Stream |
Stream |
部分终端操作:
操作 | 类型 | 目的 |
---|---|---|
forEach | 终端 | 消费流中的每个元素并对其应用Lambda。这一操作返回void |
count | 终端 | 返回流中元素的个数。这一操作返回long |
collect | 终端 | 把流归约成一个集合,比如List、Map甚至是Integer。 |
第五章 使用流
筛选与切片
- 用谓词筛选 利用filter方法。参数为Predicate
类型谓词。 - 筛选各异的元素 利用distinct方法。起到去重的作用。
- 截短流 利用limit方法,限制其长度。
- 跳过元素 利用skip方法,跳过前指定个数元素。
映射
对流中每一个元素用函数
流支持map方法,它会接受一个函数作为参数。这个函数不限于获取序列元素的属性值,还可以实现将序列元素转换成另外一个对象,也就是类型转换。
流的扁平化
flatMap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接成为一个流。
查找和匹配
匹配的几种方法:
- allMatch
- anyMatch
- noneMatch
- findFirst
- findAny
短路求值:有些操作不需要处理整个流就能得到结果。
查找的几种方法:
- findAny
- findFirst
何时使用findFirst和findAny
如果你对查找的结果没有顺序要求,那么可以通过findAny方法使用并行流提高查找效率。
Optional:
Optional类是一个容器类,代表一个值存在或者不存在。
有以下四个方法:
- isPresent() 将在Optional包含值的时候返回true,否则返回false。
- ifPresent(Consumer
block) 会在值存在的时候执行给定代码块。 - T get() 会在值存在时返回值,否则抛出NoSuchElement异常。
- T orElse(T other) 会在值存在时返回值,否则返回一个默认值。参数属性赋值时类似于三元运算。
归约
关键词:reduce
归约操作:将流归约成一个值。
元素求和
示例:
//有初始值
int sum = numbers.stream().reduce(0,(a,b) -> a + b);
//无初始值
int sum = numbers.stream().reduce((a,b) -> a + b);
最大值最小值
示例:
// 求最大值
Optional max = numbers.stream().reduce(Integer::max);
// 求最小值
Optional min = numbers.stream().reduce(Integer::min);
流操作:无状态和有状态
- 无状态:没有内部状态,需要和有状态进行比较理解。
- 有状态:是可能需要一个内部状态来积累结果,比如在做reduce、sum、max等操作每内部迭代一次,需要将上次迭代的结果保存下来,留作下次迭代使用,这个保存就需要内部状态来保存,所以叫有状态,以上三个方法在一次迭代得到的结果是一个int或double,所以这个内部状态是有界的,以为需要存储的值就一个,相反诸如sort或distinct等操作,进行排序或者去重的时候需要所有元素都放到内部状态中一起进行排序进行去重,这个要求是无界的,因为不知道这个流有多少个元素。
数值流
原始类型流特化
Java 8引入了三个原始类型特化流接口来避免了暗含的装箱成本:
- IntStream
- DoubleStream
- LongStream
映射到数值流:
- mapToInt:会接收Integer类型返回一个IntStream而不是Streams
,IntStream有sum、max、min、average等方法。 - mapToDouble
- mapToLong
转换回对象流:
- Stream
stream = intStream.boxed(); - Stream
stream = doubleStream.boxed(); - Stream
stream = longSteam.boxed();
默认值OptionalInt:
- OptionalInt
- OptionalDouble
- OptionalLong
以上方法是在做数值流取值时,不知道该值是否存在而设的一个接收容器。有方法orElse(),当所取得值不存在时设置一个默认值,如下示例:
// mapToInt 是转换成了对象流,并在这个对象流中取最大值(max()),如果为空则设置一个默认值为1(orElse).
int max = menu.stream().mapToInt(Dish::getCalories).max().orElse(1);
数值范围
Java 8 引入了可以用于IntStream和LongStream可以生成一定范围的所有数字的静态方法:
- range(start,end) 结果值不包含(end)
- rangeClosed(start,end) 结果值包含结束值(end)
以上方法可以链接filter方法用于筛选特定值(奇数、偶数等)。
示例:
IntStream evenNumbers = IntStream.rangeClosed(1,100).filter(n -> n % 2 == 0);
构建流
由值创建流
可以使用Stream.of,它可以接受任意数量的参数。
示例:
// 得到一个字符串流
Stream stream = Stream.of("Java 8","Lambda","In","Action");
// 得到一个空流
Stream emptyStream = Stream.empty();
有数组创建流
可以使用Arrays.stream,
示例:
int[] numbers = {2,3,5,7,11,13};
int sum = Arrays.stream(numbers).sum();
由文件生成流
Java 中用于处理文件等I/O操作的NIO API(非阻塞I/O)已更新,以便利用Stream API.
java.nio.file.Files中的很多静态方法都会返回一个流。其中一个很有用的方法是:Files.lines,它会返回一个由指定文件中的各行构成的字符串流。
有函数生成流: 创建无限流
- Stream.iterate 迭代
- Stream.generate 生成
第六章 用流收集数据
收集器简介
收集器用作高级归约
对流调用collect方法将对流中的元素触发一个归约操作(由Collector来参数化)。一般来说,Collector会对元素应用一个转换函数。
预定义收集器
主要提供三大功能:
- 将流元素归约和汇总为一个值
- 元素分组
- 元素分区
归约和汇总
在需要将流项目重组成集合时,一般会使用收集器,但凡要把流中所有项目合并成一个结果是就可以用,这个结果是任何类型的。
查找流中的最大值和最小值
两个收集器:
- Collectors.maxBy
- Collectors.minBy
汇总
Collectors类专门为汇总提供了一个工厂方法:Collectors.summingInt。它可接受一个把对象映射为求和所需int的函数,类似于map函数,示例:
int totalCalories = menu.stream().collect(summingInt.collect(Collectors.summingInt(Dish::getCalories))))
类似的还有:
- Collectors.summingLong
- Collectors.summingDouble
还有求求平均数:
- Collectors.averagingInt
- Collectors.averagingLong
- Collectors.averagingDouble
Collectors类提供了一个函数输出结果包含了上述的集中计算:总和、平均值、最大值、最小值。如下:
- Collectors.summarizingInt
- Collectors.summarizingLong
- Collectors.summarizingDouble
示例:
IntSummaryStatistics menuStatistics = menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));
//menuStatistics 输出结构 { count = 9,sum = 4300, min = 120, average = 477.777778, max = 800}
连接字符串
joining工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串。示例:
String shortMenu = menu.stream().map(Dish::getName).collec(Collectors.joining());
请注意: joining在内部使用了StringBuilder来把生成的字符串逐个追加起来。此外还要注意,如果Dish类有一个toString方法来返回菜肴名称,便无需用提取每道菜名称函数来对原流做映射就能够得到相同的结果:
String shortMenu = menu.stream().collect(Collectors.joining());
但是实际在代码里写了以后,发现并不行,不知道为啥。
输出类似如下:
porkbeefchickenfrench friesriceseason fruitpizzaprawnssalmon
如果需要逗号分割,则使用joining(", ")即可。
广义的归约汇总
Collectots.reducing工厂方法需要三个参数:
- 第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值和而言0是一个合适的值。
- 第二个参数就是每次归约操作的所使用的数的获取函数,如将菜肴转换成一个表示其所含热量的int(Dish::getCalories)。
- 第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值。
恒等函数:一个函数仅仅是返回其输入参数。
示例:
(d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2;
reducing方法要求两个参数为同一类型,返回值也是同一类型。
分组
Collectors.groupBy工厂方法
Map> dishesByType = menu.stream().collect(Collectors.groupingBy(Dish::getType));
多级分组
其实就是groupingBy嵌套 groupBy,第一级分组后,在各分组中在进行一次分组(2级分组)。
groupBy(参数一,参数二)
第一个参数:将集合按类型分组。
第二个参数:进行相应操作(任意类型)
示例:
Map typesCount = menu.stream().collect(groupingBy(Dish::getType,counting()));
// 其结果是下面的Map:
{MEAT=3,FISH=2,OTHER=4}
注意: 普通单参数groupingBy(f) (其中f是分类函数)实际上是groupingBy(f,toList())的简便写法。groupingBy方法返回的是map,而这个map的值是Optional类型的,例如Optional
把收集器的结果转换为另外一种类型
示例:
Map mostCaloricByType =
menu.stream().collect(groupingBy(Dish::getType,
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)),Optional::get
))));
大家可以看一下这个例子与上个例子的区别,在groupingBy方法中,有第一个groupingBy(Dish::getType,counting()),但是这样写得到map的值为Optional类型,所以第二个groupingBy(Dish::getType,collectingAndThen(...)),区别就是,第二个参数进行wrap操作,warp了一层collectingAndThen操作。类似的类型转换可以从这个例子中得到思路。
分区
分区是分组的特殊情况(也就是分组的子集),由一个谓词(返回一个布尔值的函数)作为分类函数,这个函数称为分区函数。
所以得到的分组Map的键类型是Boolean,即最多可以分为两组,true一组,false一组。
示例:
Map> partitionMenu = menu.stream().collect(partitionBy(Dish::isVegetarian)));
分区还有一个重载版本,partitionBy(分区函数,第二个收集器),示例:
Map>> vegetarianDishesByType =
menu.stream().collect(partitionBy(Dish::isVegetarian,groupingBy(Dish::getType)));
// 结果
{
false={FISH=[...],MEAT=[...]},
true={OTHER=[...]}
}
类似的还有:
partitionBy(Dish::isVegetarian,collectionAndThen(maxBy(...),Optional::get)))
注意: partitionBy第一个参数也就是分区函数,是一个返回布尔值的函数,如不是则会报错。
剩下省略有时间再补。。。