java8 基础知识
java8 Lambda表达式
从苹果List中找出红颜色的苹果
- 定义一个Bean类
Apple.java
|
|
- 定义一个接口
AppleService.java
,里面包含一个过滤红色苹果的方法
|
|
- 实现方法
AppleServiceImpl.java
|
|
- 测试:
|
|
- [x] 上面这段代码在优化一下
|
|
- [x] 输出结果
|
|
此时如果增加新的需求要过滤绿色怎么办
- 如果此时增加需求要过滤绿色的苹果,怎么办?
- 可能会在AppleService中在增加一个方法
|
|
这样就造成了代码的冗余
使用接口Predicate
- 这个类在 java.util.function.Predicate 单独写出来为了清晰
|
|
你也可以单独定义一个接口,重新定义其中的方法名,使其清晰一点。
定义一个接口
Predicate.java
|
|
- 修改AppleService中的方法
|
|
- 测试类:
|
|
优化一下:
|
|
使用大招 Lamda表达式 重构之前的方法,以过滤红苹果示例:
- 重构之前:
|
|
- [x] 重构之后:
|
|
- [x] 再重构之后
|
|
- [x] 再重构之后:
|
|
- [x] 再重构之后:
|
|
- [x] 再重构之后:
|
|
注意: ★★★★★★
上面代码中:TestApple 代表的是当前类的名字。
概念介绍:谓词
上面的代码传递了方法TestApple :: isReApple
(它接受参数apple并返回一个boolean值)给filterApple,后者则希望接受一个Predicate<Apple>
参数。
谓词(predicate):
在数学上尝尝用来表示一个类似函数的东西,它接受一个参数值,并返回true或false,在后面你会看到,java8也会允许你写Function<Apple,Boolean>
,但是用Predicate是更标准的方式,效率也会高一点,避免了boolean封装在Boolean装箱的过程。
简单点说谓词(就是一个返回boolean值的函数)
- 完整版示例代码:
|
|
从方法传递到 lambda
- 把方法作为值来传递显然很有用,但要是为了类似于
isRedApple()
和isGreenApple()
这种只用一两次的短方法写一堆的定义有点烦人。 - 所以在java8引入了一套新记法(匿名函数或者lambda),你可以这样写
|
|
- [x] 或者
|
|
- [x] 再或者
|
|
- 所以你甚至不需要为只用一次的方法写定义;
- 代码更干净,更清晰
- 因为你用不着去找自己到底传递了什么代码。
- 但是如果Lambda的长度多以几行(它的行为也不是一目了然)的话,你还是应该用方法引用来指向一个有描述性名称的方法,而不是使用匿名的Lambda。
- 应该以代码的清晰度为准绳
本来java加上filter和几个相关的东西作为通用库方法就足以让人满意了。
- 比如:
|
|
- [x] 这样你甚至都不需要写filterApple(List
list,Predicatet 了predicatet)
- 比如
|
|
- [x] 直接使用调用库方法filter
|
|
- 但是为了更好地利用并行,java的设计师没有这么做。java8中有一整套新的集合API——Stream,他有一套函数式类似于filter的操作。
- 比如 map、reduce 还有接下来要学习的Collections和Streams之间做转换的方法。
流
- 先简单的让你体验一下Stream和Lambda表达式顺序或并行地从一个列表里筛选红颜色的苹果
|
|
- 做了几个简单的测试
- stream()比在数据量低于百万的时候 效率第一倍
集合的操作更加的人性化
- jdk1.8之前,我们对集合的一些操作比如,排序,在之前,我们使用排序
|
|
- [x] 在1.8之后,我们可以 集合 . sort() 示例:
|
|
- [x] 请出神器 Lambda 示例:
|
|
默认方法
- 在java8里,你可以直接对List调用sort方法
- 它使用java8 List接口中如下所示的默认方法实现的,它会调用Collections.sort静态的方法。
|
|
第2章 通过行为参数化传递代码
重构你的垃圾代码
- 定义一个谓词方法
|
|
- 在列中定义一个公共的静态的泛型方法
|
|
- [x] 测试类
|
|
此时如果
- [x] 使用Lambda表达式
|
|
用Runnable 执行代码块
|
|
- [x] Lambda 重构后
|
|
这里要说明的,像
run()
方法一样,方法没有参数,那么书写Lamda表达式的时候,写一对 空括号()
第3章 Lambda 表达式
Lambda 表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
- 匿名:
- 说其匿名,因为它不像普通的方法那样有一个明确的名称:写得少而想得多
- 函数
- 说其实函数,是因为Lambda函数不像方法那样属于某个特定的类,但和方法一样,lambda有参数列表、函数主体、返回类型、还可能有可以抛出的异常列表
- 传递
- Lambda表达式可以作为参数传递给方法或存储在变量中
- 简洁
- 无需像匿名类那样写很多模板代码
详解 Lambda 表达式
- Lambda 没有return语句 ,因为已经隐含了return
|
|
- Lambda表达式可以包含多行语句,用一对大括号包裹
|
|
- 如果return是一个控制流语句,要让Lambda有效,需要添加花括号
{}
包裹
|
|
- Lambda 没有参数,并返回String作为表达式,这种情况下不能有
{}
|
|
java中有效的表达式
|
|
函数式接口
- 我们之前声明的
Predicate<T>
接口就是一个函数式接口- 因为Predicate仅仅定义了一个抽象方法
|
|
- 简单点说,函数式接口就是只定义一个抽象方法的接口。
- 在java API中有很多函数式接口,例如
Comparator
和Runnable
接口
接口可以拥有默认方法,不管接口中有多少默认方法,只要接口中只定义了一个抽象方法(有且仅有一个),它就是一个函数式接口,重载的方法也不行哦。
- 如果你声明了一个函数式接口,用什么办法检查你是否声明的正确呢。
- 使用@FunctionalInterface 注解(新版的API才有)
如果你声明了一个函数式接口,而它却不是函数式接口,编译器将返回一个提示原因的错误。
- 例如:错误提示可能是:
- Multiple non-overriding abstract methods found in interface Foo, 表名存在多个抽象方法。
- @FunctionalInterface 并不是必须的,但是对于设计函数式接口是比较好的做法。
- 它就像是@Override标注表示方法被重写了一样。
- 看到这里 你可能还是不清楚为什么要要定义函数式接口(仅含有一个抽象方法的接口),因为,你定义多个方法,lambda无法确定是哪一个方法。
附录2:Java8中常用的函数式接口:
函数式接口 | 函数描述符 | 原始烈性特化 |
---|---|---|
Predicate<T> |
T -> boolean | IntPredicate<T>, LongPredicate<T>, DoublePredicate<T> |
Consumer<T> |
T -> void | IntConsumer, LongConsumer, DoubleConsumer |
Function<T,R> |
T -> R | IntFunction<R>, IntToDoubleFunction, IntToLongFunction, LongFunction<R>, LongToDoubleFunction, LongToIntFunction, DoubleFunction<R>, ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T> |
Supplier<T> |
() -> T | BooleanSupplier, BooleanSupplier, BooleanSupplier, BooleanSupplier |
UnaryOperator<T> |
T -> T | IntUnaryOperator, DoubleUnaryOperator, LongUnaryOperator |
BinaryOperator<T> |
(T,T) -> T | IntBinaryOperator, DoubleBinaryOperator, LongBinaryOperator |
BiPredicate<L,R> |
(L,R) -> boolean | |
BiConsumer<T,U> |
(T,U) -> void | ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T> |
BiFunction<T,U,R> |
(T,U) -> R | ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U> |
函数描述符
- 函数式接口的抽象方法的签名基本就是Lambda表达式的签名,我们将这种抽象方法叫作函数描述符。
把Lambda 付诸实践,环绕执行模式
- 第一步:记得行为参数化
- 方法传参传递行为(也就是方法)
- 第二步: 使用函数式接口来传递行为
- 第三步:执行一个行为
- Lambda 表达式允许你直接内联系
- 再次阐述一下流程
|
|
以上四个步骤就是应用环绕执行模式所采取的四个步骤
使用函数式接口
- 函数式接口
+ 函数式接口只定义了一个抽象方法(有且仅有一个) - java8设计师在
java.util.function
包中引入了几个新的函数式接口。
Predicate(谓词)
java.util.function.Predicate<T>
接口定义了一个叫 test()的抽象方法,它接受泛型<T>
对象,并返回一个boolean 。- 在你需要表示一个涉及类型
<T>
的布尔表达式时,你就可以使用此接口。
|
|
使用javaApi自带的Predicate函数式接口,代码示例:
|
|
Consumer(消费者)
java.util.function.Consumer<T>
定义了一个accept的抽象方法它接受泛型<T>
的对象,没有返回值 void。- 在你需要访问类型
<T>
的对象,并对其执行某些操作,就可以使用此接口 。
|
|
使用javaApi自带的Consumer函数式接口,代码示例:
|
|
Function
java.util.function.Function<T,R>
接口定义了一个apply的抽象方法它接受泛型<T>
的对象,它接受一个泛型<T>
的对象,并返回一个泛型<R>
的对象。- 如果你需要定义一个Lambda,将输入的对象的信息映射到输出,就可以使用这个接口 。
- 比如你传入一个
List<String>
返回一个List<integer>
- 比如你传入一个
|
|
使用javaApi自带的Function函数式接口,代码示例:
|
|
原始类型特化
- java类型有两种
- 引用类型
- 基本数据类型
- 基本数据类型转换成引用类型,是装箱的过程。
- 装箱的本质
- 将基本数据类型包裹起来,并保存在堆里。
- 因此装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。
java8为我们的函数式接口带来了一个专门的版本,以便在输入和输出值都是基本数据类型时避免自动装箱的操作。
使用javaApi自带的IntPredicate函数式接口,代码示例:
|
|
使用javaApi自带的IntPredicate函数式接口,代码示例:
|
|
使用javaApi自带的BiFunction函数式接口,代码示例:
|
|
使用javaApi自带的BiFunction函数式接口,代码示例:
|
|
一般来说,针对专门的输入输出参数类型的函数式接口的名称都要加上对应的原始类型前缀。
比如:DoublePredicate、IntConsumer、ingBinaryOperator,IntFunction等
Function接口还有针对输出参数类型的变种:
ToIntFunction<T>
、IntToDoubleFunction等
java8常用函数式接口
函数式接口 | 函数描述符 | 原始烈性特化 |
---|---|---|
Predicate<T> |
T -> boolean | IntPredicate<T>, LongPredicate<T>, DoublePredicate<T> |
Consumer<T> |
T -> void | IntConsumer, LongConsumer, DoubleConsumer |
Function<T,R> |
T -> R | IntFunction<R>, IntToDoubleFunction, IntToLongFunction, LongFunction<R>, LongToDoubleFunction, LongToIntFunction, DoubleFunction<R>, ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T> |
Supplier<T> |
() -> T | BooleanSupplier, BooleanSupplier, BooleanSupplier, BooleanSupplier |
UnaryOperator<T> |
T -> T | IntUnaryOperator, DoubleUnaryOperator, LongUnaryOperator |
BinaryOperator<T> |
(T,T) -> T | IntBinaryOperator, DoubleBinaryOperator, LongBinaryOperator |
BiPredicate<L,R> |
(L,R) -> boolean | |
BiConsumer<T,U> |
(T,U) -> void | ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T> |
BiFunction<T,U,R> |
(T,U) -> R | ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U> |
附录1:Lambda及函数式接口的例子:
使用案例 | lambda例子 | 对应函数式接口 |
---|---|---|
布尔表达式 | (List |
Predicate<List<String>> |
创建对象 | () -> new Fan() | Supplier<Fan> |
消费一个对象void | (Fan f) -> sys…out(f.toString) | Consumer<Fan> |
从一个对象中选择提取 | (String s) -> s.length() | Function<String,Integer> 或 ToIntFunction<String> |
合并2个值 | (int a , int b) -> a+b | IntBinaryOperator |
比较2个对象 | (Fan a , Fan b) -> a.getXX().compareTo(b.getXX()) | Comparator<Fan> 或 BiFunction<Fan,Fan,Integer> 或 ToIntBiFunction<Fan.Fan> |
异常、Lambda,还有函数式接口是什么鬼
注意: 任何函数式接口都不允许抛出受检异常
- 如果你需要Lambda表达式抛出异常,有两种方式:
- 定义一个自己的函数式接口,并声明受检异常
- 将Lambda表达式包裹在一个
try/catch
块中
自定义函数式接口,代码示例:
|
|
- 但是你可能使用一个接受函数式接口的API,比如Function
,没有办法自己创建一个,在这种情况下,你可以显式的捕获受检异常
|
|
类型检查 类型推断 以及限制
类型检查
- 类型检查过程可以分解为如下几个步骤:
- 首先,你要找出filter方法的声明
filter(List<Apple> inventory,Predicate<Apple> p)
- 第二,要求它是
Predicate<Apple>
(目标类型)对象的第二个正式参数filter(List<Apple> inventroy , Predicate<Apple> p)
- 第三,
Predicate<Apple>
是一个函数式接口,定义了一个叫做test的抽象方法boolean test(Apple apple)
- 第四,test方法描述了一个函数描述符,他可以接受一个Apple,并返回一个boolean
Apple -> boolean
- 最后filter的任何实际参数都必须匹配这个要求
filter(inventory,(Apple apple) -> a.getWeight()>150);
- 首先,你要找出filter方法的声明
如果Lambda表达式抛出一个异常,那么抽象方法所声明的Throws语句要必须与之匹配
类型推断
|
|
当Lambda表达式仅有一个类型需要推断的参数时,参数名称两边的括号也可以省略。
使用局部变量
- 局部变量必须显式的被声明为final
对局部变量的限制
- 实例变量和局部变量有一个关键不同
- 实例变量都储存在堆中
- 局部变量存储在栈上
方法引用
|
|
管中窥豹
- 方法引用可以被看做仅仅调用特定方法的Lambda的一种快捷写法。
它的基本思想:
如果一个Lambda代表的只是”直接调用这个方法“,那最好还是用名称来调用它,而不是去描述如何调用它。
方法引用就是,让你根据已有的方法实现来创建Lambda表达式。
Lambda及其等效方法引用的例子
Lambda | 等效的方法引用 |
---|---|
( Apple a ) -> a.getWeight() | Apple :: getWeight |
() -> Thread.currentThread().dumpStack() | Thread.currenThread() :: dumpStack |
( str , i )-> str.subtring(i) | String :: substring |
( String s )-> System.out.pringln(s) | System.sout :: println |
如何构建方法引用
方法引用有三类
- 指向静态方法的方法引用
- 例如Integer的parseInt方法,写作
Integer :: parseInt
- 例如Integer的parseInt方法,写作
- 指向任意类型示例方法的方法引用
- 例如String的length方法,写作
String :: length
- 例如String的length方法,写作
- 指向现有对象的实例方法的方法引用
|
|
构造函数引用
- 对于一个现有的构造函数,你可以使用它的名称和它的关键字new来创建它的一个引用, ClassName :: new
Supplier<Apple> s = Apple::new;
- 它的功能与指向静态方法的引用类似, 它适合Supplier的签名
() -> Apple
|
|
- 如果你的构造函数的签名是Apple(Integer weight), 那么它就适合Function接口的签名
|
|
- 一个由Integer构成的List中的每个元素都通过我们定义的map方法传递给了Apple的构造函数,得到一个不同重量的苹果的List
|
|
- 如果你有一个具有两个参数的构造函数Apple(String color , Integer weigth),那么它就适合BiFunction接口的签名
|
|
Lambda和方法引用实战
第一步:传递代码
|
|
第二步:使用匿名类
|
|
使用Lambda表达式
|
|
使用方法引用
|
|
复合Lambda表达式的有用方法
比较器复合
逆序
- 比如你要对苹果的重量递减
.reversed()
|
|
比较器链
- 比如你要对苹果的重量递减,但是你发现欧两个苹果一样重怎么办,你可以在提供一个Comparator
.thenComparing(...)
|
|
谓词复合
- 谓词接口包括三个方法:
- negate
- 使用
negate()
方法返回一个谓词接口的非
- 使用
- and
- 可以使用
and(...)
方法连接 Lambda 表达式
- 可以使用
- or
- 可以使用
or(...)
方法表达是它或是它
- 可以使用
- negate
|
|
- 组合谓词,表达既是红苹果又是绿苹果
|
|
- 进一步组合谓词,要么是中(150g以上)的红苹果,要么是绿苹果
|
|
注意:
and和or方法是按照在表达式链中的位置,从左到右确定优先级的。
因此,a.or(b).and(c)
可以看作( a || b ) && c
函数复合
- 最后你还可以吧Function函数式接口所代表的Lambda表达式复合起来。
- Function函数式接口为此配备了
andThen()
和compose()
两个默认方法,他们都会返回Function的一个实例。
|
|
- 不知道你是否看懂了,说下这两个方法
a.andThen( b )
先执行 a,然后再执行 ba.compose(b)
先执行 b,然后再执行 a
f.andThen(g)
|
|
输入 | f.andthen(g) |
结果 | ||
---|---|---|---|---|
1 | ——f—> | 2 | ——g——> | 4 |
2 | ——f—> | 3 | ——g——> | 6 |
3 | ——f—> | 4 | ——g——> | 8 |
f.compose(g)
|
|
输入 | f.compose(g) |
结果 | ||
---|---|---|---|---|
1 | ——f—> | 2 | ——g——> | 3 |
2 | ——f—> | 4 | ——g——> | 5 |
3 | ——f—> | 6 | ——g——> | 7 |
实际应用:
- 比如你有一个工具类,用来处理文本。
Letter.java
|
|
|
|
- 结果:
|
|
- 只加抬头和落款,而不做拼写检查:
|
|
- 结果:
|
|
小结
- Lambda表达式可以理解为一种匿名函数:它没有名称、但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表
- Lambda表达式可以让你简洁地传递代码
- 函数式接口就是仅仅声明了一个抽象方法的接口
- 只有在接受函数式接口的地方才可以使用Lambda表达式
- 只有在接受函数式接口的地方才可以使用Lambda表达式
- Lambda表达式允许你直接内联,为函数是接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例
- java8自带一些常用的函数式接口,放在
java.util.function
包里,包括Predicate<T>
、Function<T,R>
、Supplier<T>
、Consumer<T>
、和BinaryOperatory<T>
,详情见函数式接口里的表。 - 为了避免装箱操作,对
Predicate<T>
和Function<T,R>
等通用函数式接口的原始类型特化,IntPredicate
、IntToLongFunction
等 - 环绕执行模式(即在方法所必须的代码中间,你需要执行点什么操作,比如资源分配和清理)可以配合Lambda提高灵活性和可重用性。
- Lambda表达式所需要代表的类型成为目标类型
- 方法引用让你重复只是用现有的方法实现并直接传递它们
Comparator
、Predicate
和Function
等函数式接口都有几个可以用来结合Lambda表达式的默认方法。
第二部分 函数式数据处理
第4章 引入流
流是什么
- 流是java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句表达而不是临时编写一个实现)
- 用两个例子来看下:
|
|
java 7
|
|
java8
|
|
- 为了利用多核架构并行执行这段代码,你只需要吧
stream()
换成parallelStream()
|
|
- 使用java8的新方法有几个显而易见的好处
- 代码是以声明性方式写的
- 说明想要完成什么(筛选热量低的菜肴),而不是说明如何实现一个操作(利用循环和if条件等控制语句)
- 这种方法加上行为参数化让你可以轻松应对变化的需求:你很容易创建一个代码版本,利用Lambda表达式筛选高卡路里的菜肴,而不用赋值粘贴代码
- 你可以将几个基础操作链接起来,来表达复杂的数据处理流水线(filter后面接上sorted、map和collect操作),同事保证代码清晰可读。
- 代码是以声明性方式写的
小结
- 声明性——更简洁、更易读
- 可复合——更灵活
- 可并行——性能更好
流简介
流的定义
从支持数据处理操作的源生成的元素序列
- 元素序列
- 就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。
- 集合是数据结构:
- 它的主要目的是以特定的时间/控件复杂度存储和访问元素(如ArrayList和LinkedList)
- 流的目的在于计算
- 比如filter、sorted和map
- 集合讲的是数据,流讲的计算
- 源
- 流会使用一个提供数据的源,如集合、数组、或输入/输出资源
- 从有序集合生成流时会保留原有的顺序。
- 由列表生成的流,其元素顺序与列表一致
- 数据处理操作
- 流的数据处理功能支持类似与数据库的操作,以及函数式编程语言中的常用操作,如:filter、map、reduce、find、match、sort等。
- 流操作可以顺序执行,也可以并行
- 流水线
- 很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线
- 流的操作可以看做对数据源进行数据库式查询
- 内部迭代
- 与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。
流与集合
只能遍历一次
- 和迭代器类似,流只能遍历一次。
|
|
外部迭代与内部迭代
- 集合:用for-each循环外部迭代
|
|
- 集合:用背后的迭代器做外部迭代
|
|
- 流内部迭代
|
|
流操作
|
|
- 可以链接起来的流操作称为:中间操作
- 关闭流的操作称为:终端操作
中间操作
- 诸如filter和sorted等中间操作会返回另外一个流,这让多个操作可以连接起来形成一个查询。
- 中间操作不会执行任何处理,除非触发一个终端操作。
终端操作
- 终端操作会从流的流水线生成结果,其结果是任何不是流的值,可以使List,Integer,甚至void
使用流
- 流的使用一般包括三件事:
- 一个数据源(如集合)来执行一个查询
- 一个中间操作链,形成一条流的流水线
- 一个终端操作,执行流水线,并能生成结果
中间操作
操作 | 类型 | 返回类型 | 操作参数 | 函数描述符 |
---|---|---|---|---|
filter | 中间 | Stream<T> |
Predicate<T> |
T -> boolean |
map | 中间 | Stream<R> |
Function<T,R> |
T -> R |
limit | 中间 | Stream<T> |
||
sorted | 中间 | Stream<T> |
Comparator<T> |
(T,T) -> int |
distinct | 中间 | Stream<T> |
终端操作
操作 | 类型 | 目的 |
---|---|---|
forEach | 终端 | 消费流中的每个元素并对其应用Lambda,这一操作返回void |
count | 终端 | 返回流中元素的个数,这一操作返回long |
collect | 终端 | 把流归约承一个集合,比如List,Map甚至是Integer |
小结
- 流是从支持数据处理操作的源生成的一系列元素
- 流利用内部迭代:迭代通过filter、map、sorted等操作被抽象掉了
- 流操作有两类:中间操作和终端操作
- filter和map等中间操作会返回一个流,并可以链接在一起。可以用它们来设置一条流水线,但并不会生成任何结果
- forEach和Count等终端操作会返回一个非流的值,并处理流水线以返回结果
- 流中的元素是按需计算的
第5章
筛选和切片
用谓词筛选
- Stream接口支持filter方法,改操作会接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包含所有复合谓词的元素的流。
筛选各异的元素
- 流还支持一个叫做distinct的方法,它会返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流。
- 简单点说就是去重
|
|
截断流
- 流支持limit方法,该方法会返回一个不超过给定长度的流。所需参数作为参数传递给limit
- 如果流是有序的,则最多会返回前n个元素
limit也可以用在无序流上,比如源是一个Set,这种情况下,limit的结果不会以任何形式排序
|
|
跳过元素
- 流还支持skip(n)方法,返回一个扔掉了前n个元素的流。
- 如果流中元素个数不足n个,则放回一个空流。
|
|
映射
对流中每一个元素应用函数
- 流支持map方法,它会接受一个函数作为参数。
- 这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射,是因为他和转换类似,但其中的细微差别在于它是“创建一个新版本”,而不是去“修改”)
|
|
- 当你不知道一个方法中的Lambda表达式应该如何写的时候,你可以这样做
- 先传入匿名内部类,然后一步一步的提炼
|
|
流的扁平化
- 如何返回一张单词表,列出里面各不相同的字符呢。
- 第一个版本,返回的是一个list,里面包含了两个string[]数组
|
|
这与我们想要的不同,我们要的是
List<String>
而现在是List<String[]>
- 尝试使用Map和Arrays.stream()
|
|
依然搞不定,返回的仍然是
List<String[]>
- 我们可以使用flatMap来解决这个问题 ☆☆☆☆☆☆
|
|
- flatMap()方法
- 让你将流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流
|
|
|
|
|
|
- 和map类似,不同的是其每个元素转换得到的是Stream对象,会把子Stream中的元素压缩到父集合中;
查找和匹配
- Stream API 通过allMatch、anyMatch、noneMatch、findFirst和findAny()方法提供了这样的工具
检查谓词是否至少匹配一个元素
- anyMatch()方法
- 查看流中是否有一个元素能匹配给定的谓词
- anyMatch()方法返回一个人boolean
- 因为返回boolean,所以是终端操作
比如查看菜单中是否有素食
|
|
检查谓词是否匹配所有元素
- allMatch()方法
- 工作原理和anyMatch()类似,但它会查看流中的元素是否都能匹配给定的谓词。
查看菜品是否健康
|
|
- noneMatch()
- 与allMatch()相反,它可以确保流中没有任何元素与给定的谓词匹配。
|
|
- 短路求值
- 有些操作不需要处理整个流就能得到结果。
查找元素
- findAny()方法
- 返回当前流中的任意元素,它可以与其他操作结合使用
|
|
- 流水线将在后台只需走一遍,并利用短路找到结果立即返回。
Optional<T>
类是一个容器类,代表一个值存在或不存在- Optional里面有几种可以迫使你显式的检查值是否存在或处理值不存在的情形的方法
方法 | 说明 |
---|---|
isPresent() |
将在optional包含值的时候返回true,反之返回false |
isPresent(Consumer<T> block) |
会在值存在时执行给定的代码块,在第3章介绍过Consumer函数式接口,他让你传递一个接受T类型参数,并返回void的Lambda表达式 |
T get() |
会在值存在时返回值,否则抛出一个NoSuchElement异常 |
T orElse(T other) |
会在值存在时防具机制,否则返回一个默认值 |
|
|
查找第一个元素
- findFirser()方法
- 工作方式类似于findAny(),查找第一个元素
|
|
那么问题来了:何时使用findFirst和findAny
- 答案是并行,找到第一个元素在并行上限制更多,如果你不关心返回的元素是哪个,请使用findAny()
- 因为*findAny()在并行流时限制较少
归并
- 使用reduce来操作更为复杂的查询
- 此类查询需要将流中的所有元素反复结合起来,得到一个值
- 这样的查询可以被归类为归约操作(将流归约承一个值)
元素求和
- 使用for-each循环来对数字列表中的元素求和
|
|
- 在这里代码有两个参数
- 总和变量的初始值,在这里是0
- 将列表中的所有元素结合在一起的操作,这里是
+
- 要是想要将所有的数字想成,而不必复制粘贴这些代码,就要用reduce
reduce()方法
- reduce()方法
- 对这种重复应用的模式做了抽象
- 它接受两个参数
- 一个初始值,这里是0
- 一个BinaryOperator
来讲两个元素结合起来产生一个新值
|
|
无初始值
reduce()
还有一个重载的辩题,它不接受初始值,但是会返回一个Optional对象
|
|
- 为什么返回一个
Optional<Integer>
考虑流中没有任何元素的情况。 - reduce操作无法返回其和,因为它没有初始值
最大值和最小值
- reduce只要接受两个参数
- 一个初始值
- 一个Lambda来把两个流元素应用到流中每个元素上
|
|
怎样用map和reduce来查询流中有多少元素
|
|
中间操作和终端操作
操作 | 类型 | 返回类型 | 操作参数 | 函数描述符 |
---|---|---|---|---|
filter | 中间操作 | Stream<T> |
Predicate<T> |
T -> boolean |
distinct | 中间操作(有状态——无界) | Stream<T> |
||
skip | 中间操作(有状态——有界) | Stream<T> |
long |
|
limit | 中间操作(有状态——有界) | Stream<T> |
long |
|
map | 中间操作 | Stream<T> |
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> |
||
forEach | 终端操作 | void |
consumer<T> |
T -> void |
collect | 终端操作 | R |
Collector<T,A,R> |
|
reduce | 终端操作(有状态——有界) | Optional<T> |
BinaryOperator<T> |
( T , T ) -> T |
count | 终端操作 | long |
付诸实践
- 当你对list中的元素进行去重的时候,可以考虑使用
toSet()
|
|
- [x] 使用
toSet()
|
|
- 连接字符串效率不高,可以考虑使用
Collectors.joining()
|
|
- [x] 使用
Collectors.joining()
|
|
- 流支持 min() 和 max() 方法,他们可以接受一个Comparator作为参数,制定计算最大或最小值要比较哪个键值
|
|
- [x] 使用 min()方法
|
|
数值流
- 在前面我们看到了可以使用 reduce() 方法计算流中元素的总和
|
|
- 这段代码的问题是,它有一个暗含的装箱成本, 每个Integer都必须拆箱承一个原始类型再进行求和
- 为此,Stream API 提供了原始类型流特化,专门处理数值流的办法
原始类型流特化
- Java8 引入了三个原始类型特化流接口
- IntStream
- DoubleStream
- LongStream
- 分别将流中的元素特化为 int 、long 和 double,从而避免暗含的装箱成本
映射到数值流
- 将流转换为特化版本的常用方法 mapToInt() 、 mapToDouble() 和 mapToLong()
- 这些方法和 map 方法的工作方式一样,只是他们返回的是一个特化流,而不是 `Stream
``
|
|
- 如果流是空的,sum()方法 默认返回0
- IntStream 还支持其他的方法方法,如 max()、min()、average() 等
转换为对象流
- 要将原始流转换为一般流,可以使用 boxed() 方法
|
|
默认值 OptionalInt
- 求和的例子很简单,因为它有一个默认值: 0
- 如果你要计算 IntStream 中的最大元素,就得换个方法了,因为0是错误的结果
- 如何区分没有元素的流和元素最大的流呢?
- 对于三种原始流特化,也分别有一个 Optional 原始类型特化版本:
- OptionalInt
- OptionDouble
- OptionalLong
|
|
- 有的时候无法区分是没有元素的流,还是最大值真的是0的流,可以显式处理 OptionalInt 去定义一个默认最大值
如果没有最大值的话,可以显式的处理 OptionalInt 去定义一个默认值
|
|
数值范围
- java8 引入了两个一个用于 IntStream 和 LongStream 的静态方法,帮助生成这种范围
- range()
- rangeClosed()
- 这两个方法都是第一个参数接受起始值,第二个参数接受结束值。
- range() 是不包括结束值的
[x,y)
- rangeClosed() 包含结束值
[x,y]
|
|
数值流应用 勾股数
构建流
由值创建流
- 可以使用静态方法 Stream.of(…) 通过显式值创建一个流
- 可以使用 empty()创建一个空流
|
|
由数组创建流
- 你可以使用静态方法 Arrays.stream(…) 从数组创建一个流
|
|
由文件创建流
- java中用与处理文件等I/O操作的NIO API(非阻塞I/O)已更新,以便利用 Stream API
java.nio.file.Files
中的很多静态方法都会返回一个流。- 一个很有用的方法 Files.readLines()
- 它会返回一个由指定文件中的各行构成的字符串流。
|
|
- [x] 上面的方法是每一行生成一个单词流,我们修改一下产生一个扁平化的单词流
|
|
由函数创建流 创建无限流
- Stream API 提供了两个静态方法从函数创建流:
- Stream.iterate(…)
- Stream.generate(…)
- 这两个操作可以创建所谓的无限流:不想固定集合创建的流那样有固定大小的流
- 由 iterate 和 generate 创建的流会用给定的函数按需创建主,因此可以无线创建下去
Stream.iterate(…)
第一个参数是初始值,第二个参数是一个Lambda表达式(UnaryOperator<T>
类型【UnaryOperator ♥ 一元运算符】)
|
|
这样得到的是一个无限流,我们使用 limit(…)方法显式的限制流的大小。
|
|
Stream.generate(…)
- 与iterate方法类似,generate方法也可以让你按需生成一个无限流。但generate不是一次对每个新生成的值应用函数的。
- 它接受一个供应源
Supplier<T>
类型的Lambda提供的值。
- [x] 示例代码
|
|
- [x] 使用 IntStream 说明避免装箱操作
|
|
- generate() 方法将是用给定的供应源,并反复的调用 getAsInt() 而这个方法总是返回2
- [x] 比较 generate() 和 iterate()
|
|
- 前面的代码创建了一个 IntSupplier 的实例
- 此对象有可变的状态:他在两个实例变量中记录了前一个数,getAsInt() 在调用时会改变对象的状态,由此在每次调用时产生新的值
注意:
- 你应该始终采用不变的方法,以便并行处理流,并保持结果正确。
- 因为你处理的是一个无限流,所以必须使用 limit 操作来显式的限制流的大小,否则,终端操作(这里是forEach)将永远计算下去
- 你不能对无限流做排序或归约,因为所有的元素都要处理,而这永远也完不成
小结
- Stream API 可以表达复杂的数据处理查询
- 你可以使用 filter、 distinct、 skip 和 limit 对流做筛选和切片
- 你可以使用 map 和 flatMap 提取或转换流中的元素
- 你可以使用 findFirst 和 findAny 方法查找流中的元素
- 你可以用 allMatch、 noneMatch 和 anyMatch 方法让流匹配给定的谓词
- 这些方法都利用了短路:找到结果就立即停止计算;没有必要处理整个流
- 你可以利用 reduce 方法将流中的所有元素迭代合并成一个结果,例如求和或查找最大元素
- filter 和 map 等操作是无状态的,他们并不存储任何状态。resuce 等操作要存储状态才能计算出一个值。 sorted 和 distinct 等操作也要存储状态 ,因为他们要把流中的所有元素缓存起来才能返回一个新的流。这种操作称为有状态操作
- 流有三种最基本的原始类型特化: IntStream 、 DoubleStream 和 LongStream ,他们的操作也有相应的特化
- 流不仅可以从集合创建,也可从数值、数组、文件以及 iterate 与 generate 等特定方法创建
- 无限流是没有固定大小的流
第6章 用流收集数据
收集器简介
预定义收集器
- 主要探讨预定义处理器的功能,也就是
Collectors
类提供的工厂方法(例如:groupingBy()
方法)创建的收集器 - 主要提供了三大功能:
- 将流元素归约和汇总为一个值
- 元素分组
- 元素分区
归约和汇总
在需要将流项目重组成集合时,一般会使用收集器(Stream方法
collect()
的参数)。在宽泛一点说,但凡要把流中所有的项目合并成一个结果时就可以用,这个结果可以使任何类型
- [x] 示例代码
|
|
- counting收集器在和其他收集器联合使用的时候特别有用,后续或细说。
查找流汇总的最大值和最小值
- 要查找流中的最大值和最小值,可以使用这两个收集器
- Collectors.maxBy(Comparator<? super T> comparator)
- Collectors.minBy(Comparator<? super T> comparator)
这两个收集器接收Comparator参数来比较流中的元素
[x] 代码示例
|
|
- 另一个常见的返回单个值的归约操作是对六中的一个数值字段求和,或者你可能想要求平均数,这种操作被称为汇总操作
- 使用收集器来表达汇总操作
汇总
- Collectors 类专门提供了一个工厂方法:
Collectors.summingInt()
,它可接受一个把对象映射为求和所需int的函数,并返回一个收集器;将该收集器传递给普通的colllect方法后及执行我们需要的汇总操作
- [x] 代码示例
|
|
Collectors.summingLong()
和Collectors.summingDouble()
方法的作用完全一样,可以用于求和字段为long或double的情况- 但是汇总不仅仅是求和;还有
Collectors.averagingInt()
,连同对应的Collectors.averagingLong()
和Collectors.averagingDouble()
可以计算数值的平均数
很度时候,你可能想要得到两个或更多这样的结果,而且你只需一次操作就可以完成,在这种情况下,你可以使用 summarizingInt 工厂方法返回的收集器。
- 通过一次 summarizing 操作你就可以查出元素的个数,并得到总和、平均值、最大值和最小值
- 代码示例:
|
|
- 同样,相应的
summarizingDouble
和summarizingLong
有相关的DoubleSummaryStatistics
,LongSummaryStatistics
连接字符串
- joining工厂方法返回的收集器会把对流中的每个对象用
toString()
方法得到的所有字符串连接成一个字符串。
注意:
joining方法在内部使用了StringBuilder
来把生成的字符串逐个追加起来。
如果Dish类中有一个toString方法来返回名称的字符串,那你就不需要函数来对流做映射就能得到相同的结果map(Dish::getName)
|
|
- 但该字符串的可读性不好,不过joinging工厂有一个重载版本可以接受元素之间的分界符
|
|
guaj广义的归约汇总
- 之前介绍的所有收集器,都是一个可以用
Collectors.reducing(...)
工厂方法定义个归约过程的特殊情况而已。 Collections.resucing(...)
工厂方法是所有这些特殊情况的一般化
|
|
- 看最后一个方法,它需要三个参数
- 第一个参数是归约操作的起始值,也是六中没有元素时的返回值,所有很显然对于数值和而言0是一个何时的值
- 第二个参数是返回int的函数,将对象转换为表示某个值的int
- 第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值
- [x] 代码示例:
|
|
收集和归约
- Stream接口的collecthereduce方法有和不同,这两种方法通常会获得相同的结果
|
|
- 一个语义问题,一个实际问题。
- 语义问题在于:
- reduce方法旨在将两个值结合起来生成一个新值,他是一个不可变的归约。
- collect方法的设计就是要改变容器,从而累积要输出的结果。
- 这意味着上面得到代码片段是滥用reduce方法,因为它在原地改变了作为累加器的List。以错误的语义使用reduce方法还会造成一个实际问题:这个归约过程不能并行工作,因为由多个线程并发修改同意个数据结构可能会破会List本身。在这种情况下,如果你想要线程安全,就需要多次分配一个新的List,而对象分配又会影响性能。
- 这就是collect方法特别适合表达可变容器上的归约的原因,更关键的是它适合并行操作。
收集框架的灵活性:以不同的方法执行同样的操作
|
|
根据情况选择最佳解决方法
|
|
- 函数式编程(特别是java8的Collections框架中加入的基于函数式风格原理设计的新API)通常提供了多种方法来执行统一操作。这个例子说明,收集器在某种程度上比Stream接口上直接提供的方法用起来更复杂,但好处在于他们能提供更高水平的抽象和概括,也更容易重用和自定义
- 始终选择最专门化的一个。
- 因为无论从性能上看,还是可读性上看,这都是一个最好的决定。
分组
- 用
Collectiors.groupingBy(...)
工厂方法返回的收集器可以轻松完成分组public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier)
|
|
- 分组函数不一定想方法引用那样可用,,因为你想用以分类的条件可能比简单的属性访问器要复杂。
- 由于作者没有吧这个操作写成一个方法,你无法使用方法引用,但你可以吧这个逻辑写成Lambda表达式
|
|
多级分组
- 我们可以使用由多参版本的Collections.groupingBy工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数。
- 所以要进行二级分组的话,我们可以将一个内层groupingBy传递给外层groupingBy,并定义一个为流中项目分类的二级标准
|
|
要注意:
普通的单参数groupingBy(f)
(其中f是分类函数)实际上是groupingBy( f , toList( ) )的简便写法
|
|
- 这个分组中的结果显然是一个map,以Dish的类型为键,以包装了该类型中热量最好的Dish的
Option<Dish>
作为值
将收集器中的结果转换为另一种类型
- 因为分组操作中的Map结果中的每个值上包装的Optional没什么用,可以使用
Collectors.collectingAndThen(...)
工厂方法返回的收集器 - 这个工厂方法接受两个参数
- 要转换的收集器
- 转换函数
- 返回另一个收集器
- 这个收集器相当于旧收集器的一个包装,collect操作的最后一步就是将返回值用转换函数做一个映射
|
|
与groupingBy联合使用的其它收集器的例子
- 一般来说,通过groupingBy()工厂方法的第二个参数传递的收集器将会对分到同一组中的所有流元素执行进一步归约操作。
- [x] 代码示例:求出所有菜肴热量融合,对每一组Dish求和
|
|
与
Collectors.groupingBy(...)
联合使用的另一个收集器是 Collectors.mapping(Function mapper,Collector downstream) 方法生成的。
- 这个方法接受两个参数
- 一个函数对流中元素做变换
- 另一个将变换的结果对象收集起来。
- 目的是在累加之前对每个元素应用一个映射函数,这样就可以让接受特定类型元素的收集器适应不同类型的对象
- [x] 代码示例: 想知道每种Dish,菜单中都有哪些CaloricLevel,可以将groupingBy(…)和mapping(…)收集器结合起来
|
|
- 传递给映射方法的转换函数将Dish映射成了它的 CaloricLevel:生成的CaloricLevel流传递给一个toSet收集器,它和toList()类似,不过时将流中的元素累积到一个Set而不是List中,以便仅保留各不相同的值。
此处有个问题,对于返回的Set是什么类型并没有任何保证,但是通过
Collectors.toCollection(...)
,你就可以更多的控制
- [x] 代码示例: 你可以传递一个构造函数引用来要求HashSet
|
|
分区
- 分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它被称为分区函数
- 分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以分为两组
- true 是一组
- false 是一组
Collectors.partitioningBy(Predicate predicate)
根据…区分
- [x] 代码示例:
|
|
注意
用同样的区分谓词,对菜单List创建的流作筛选,然后把结果收集到另外一个List中也可以获得相同的结果
- [x] 代码示例
|
|
分区的优势
- 分区的好处在于保留了分去函数返回true或false的两套流元素列表。
- 如果你要得到非素食Dish的list,你可以使用两个筛选操作来访问partitionedMenu这个map中的false键的值:一个利用谓词,一个利用谓词的非。
partitioningBy(Predicate predicate)
工厂方法有一个重载版本,可以传递而第二个收集器
- [x] 代码示例
|
|
- 再结合前面的代码举个例子
- 找出素食和非素食中热量最高的菜
- [x] 代码示例
|
|
Collectors.partitioningBy(Predicate predicate,Collector downStream)
|
|
将数字按质数和非质数分区
什么是质数?
- 只有1和它本身两个约数的数,叫质数。
- 如:2÷1=2,2÷2=1,所以2的约数只有1和它本身2这两个约数,2就是质数。)
先介绍两个方法,在第5张介绍过,再次回顾一下
方法 | 说明 |
---|---|
range() |
生成一组数值范围,不包括结束值的 [x,y) |
rangeClosed() |
生成一组数值范围,包括结束值的 [x,y] |
- [x] 定义一个判断是否为质数的谓词方法
|
|
- [x] 一个简单的优化是仅测试小于等于待测数平方根的因子
|
|
- 为了把前n个数字分为质数和非质数,只要创建一个包含这n个数的流,用刚刚写的isPirme()方法作为谓词,再传给Collectors.partitioningBy()收集器归约就好了
|
|
Collectors 类的静态方法
工厂方法 | 返回类型 | 用于 | 示例 |
---|---|---|---|
toList | List<T> |
把流中的所有数据元素收集到List集合中。 | stream.collect(toList()); |
toSet | Set<T> |
把流中的所有数据元素收集到Set集合中,以维持Set自身的特性,即不会出现重复项。 | stream.collect(toSet()); |
toCollection | Collection<T> |
把流中的数据元素收集你所指定的集合中 | list.stream().collect(toCollection(ArrayList::new)); |
counting | Long |
计算流中元素的个数 | list.stream().collect(counting()); |
summingInt | Integer |
对流中所有元素上指定的整数属性求和 | list.stream().collect(summingInt(User::getAge)); |
averagingInt | Double |
对流中所有元素上指定的整数属性求平均数 | list.stream().collect(averagingInt(User::getAge)); |
summarizingInt | IntSummaryStatistics |
收集流中所有元素上指定的整数属性的统计值,包括最大值、最小值、总数、平均值 | list.stream().collect(summarizingInt(User::getAge)); |
joining | String |
连接流中元素上指定的属性 | list.stream().map(s -> s).collect(joining(“-“)) |
maxBy | Optional<T> |
使用指定的比较器去比较得到流中所有元素上指定属性的最大值 | list.stream().collect(maxBy(Comparator.comparing(String::length))) |
minBy | Optional<T> |
使用指定的比较器去比较得到流中所有元素上指定属性的最小值 | list.stream().collect(minBy(Comparator.comparing(String::length))) |
reducing | 规约操作产生的类型 | 从一个累加器的初始值开始,使用BinaryOperator与流中的元素逐个集合,最后将流规约为单个值 | list.stream().collect(reducing(0, UserVO::getAge, Integer::sum)); |
collectingAndThen | 转换函数返回的类型 | 对最终结果转换为另一种类型 | list.stream().collect(collectingAndThen(Collectors.toList(), List::size)); |
groupingBy | Map<k, List<T>> |
根据指定的属性来分组 | views.stream().collect(groupingBy(String::length)); |
partitioningBy | Map<boolean, List<T>> |
根据指定的属性来分区 | views.stream().collect(partitioningBy(str -> str.startsWith(“ws”)) |
收集器接口
- 要开始使用Collector接口,
- 要先了解Collector接口是如何定义的
- 以及他的方法所返回的函数在内部是如何为collect方法所用的
Collector 接口
|
|
- 本列表适用以下定义:
- T 是流中要收集的项目的泛型
- A 是累加器的类型,累加器是在收集过程中用于累积部分结果的对象
- R 是收集操作得到的对象(通常但并不一定是集合)的类型
- [x] 我们可以实现一个
ToListCollector<T>
类,将Stream中的所有元素收集到一个List 里面,它的签名如下:
|
|
理解 Collector接口声明的方法
- 前四个方法都会返回一个会被collect方法调用的函数,而第五个方法characteristics 则提供了一系列特征,也就是一个提示列表,告诉collect方法在执行归约操作的时候可以使用那些优化(比如并行化)
建立新的结果容器:
supplier()
方法
- supplier() 方法必须返回一个空的Supplier,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,共数据收集过程使用。
- 在我们的ToListCollector中,supplier返回一个空的List
|
|
将元素添加到结果容器: accumulator() 方法
- accumulator() 方法会返回执行归约操作的函数
- 当遍历到流中第n个元素时,这个函数执行时会有两个参数:
- 保存归约结果的累加器(已收集了流中的 n-1 个项目)
- 第 n 个元素本身
- 该函数返回void,,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素的想过
|
|
对结果容器应用最终转换: finisher() 方法
- 在遍历完流后,finisher()方法必须返回在累积过程的最后要调用的一个参数,以便累加器对象转换为整个集合操作的最终结果
- 像ToListCollector()的情况一样,累加器恰好符合预期的最终结果,因此无需转换
|
|
合并两个结果容器: combiner() 方法
- combiner() 方法会返回一个供归约操作使用的函数,它定义了对流的各个部分进行并行处理时,各个子部分古月所得的累加器要如何合并
|
|
- 有了 combiner() 方法就可以对流进行并行归约了
characteristics() 方法
- characteristics() 方法会返回一个不可变的Characteristics集合,他定义了收集器的行为
- 尤其是关于刘是否可以并行归约,以及可以使用那些优化的提示
- characteristics 是一个包含三个项目的枚举
- UNOROERED
- 归约结果不受流中项目的遍历和累积顺序的影响
- CONCURRENT
- accmulator 函数可以从多个线程同事调用,且该收集器可以并行归约流
- 如果收集器没有标为UNOROERED,那它仅仅在用于无需数据源时才可以并行归约
- IDENTIFITY_FINISH
- 这表明完成器方法返回的函数是一个恒等函数,可以跳过
- 这种情况下,累加器对象将会直接用作归约过程的最终结果,这也意味着,将累加器A不加检查地转换为R是安全的
- UNOROERED
全部融合到一起
|
|
- [x] 进行自定义收集而不去实现 Collector
- 对于IDENTITY_FINISH的手机操作,还有一种方法可以得到同样的结果而无需从新实现Collector接口。
- Stream 有一个重载的collect方法可以接受另外三个函数【supplier、accumulator和combiner】,其语义和Collector接口的相应方法返回的函数完全相同
|
|
开发你自己的收集器以获得更好的性能
仅用质数做除数
- 之前求素数的方法还可以进行优化,看被测试数是否能够被质数整除
定义一个方法
|
|
- 利用这个方法,你就可以优化isPrime方法,只用不大于被测数平方根的质数去测试了
|
|
第一步: 定义Collector类的签名
- 从类签名开始,Collector接口的定义
|
|
- 其中T,A 和 R 分别是流中元素的类型、用于累积部分结果的对象类型,以及collect操作最终结果的类型。
- 这里应该收集 Integer 流,而累加器和结果类型则都是
Map<Boolean,List<Integer>>
- 键是true和false, 值则分别是质数和非质数的List
|
|
第二步:实现归约过程
- 接下来你需要实现Collector接口中的五个方法。supplier方法会返回一个在调用时候创建累加器的函数
|
|
- 这里不但创建了用作累加器的Map,还为true和false两个键下面初始化了对应的空列表
- 在收集过程中会把质数和非质数分别添加到这里。
- 收集器最重要的方法是accumulator() ,因为它定义了如何收集流中元素的逻辑。
|
|
第三步: 让收集器并行工作(如果可能)
- 下一个方法要在并行收集时把两个部分累加器合并起来,这里,它只需要合并两个Map,即将第二个Map中质数和非质数列表中的所有数字合并到第一个Map对应的列表中就行了
|
|
- 实际上 这个收集器是不能并行使用的,因为该算法本身是是顺序的
第四步: finisher方法和收集器的characteristics方法
- 这两个方法的实现都很简单,前面说过,accumulator正好就是收集器的结果,用不着进一步转换,那么finisher方法就返回identity函数
|
|
|
|
-[x] 完整代码示例
|
|
小结
- collect是一个终端操作,它接受的参数是将流中元素累积到汇总结果的各种方式(称为收集器)
- 预定义收集器包括将流元素归约和汇总到一个值,例如计算最小值、最大值或平均值,这些收集器总结在前面的
Collectors 类的静态方法
- 预定义收集器可以用groupingBy对流中元素进行分组,或用partitioningBy进行分区
- 收集器可以高效的复合起来,进行多级分组、分区和归纳
- 你可以实现Collector接口中定义的方法来开发你自己的收集器