第七章 Lambda和Stream
第42条 Lambda优先于匿名类
-
在Java8中,形成了“带有单个抽象方法的接口是特殊的,值得特殊对待”的观念。这些接口现在被称作函数接口,Java允许利用Lambda表达式创建这些接口的实例。
-
删除所有Lambda参数的类型吧,除非它们的存在能够使程序变得更加清晰
-
与方法和类不同的是,Lambda没有名称和文档;如果一个计算本身不是自描述的,或者超出了几行,那就不要把它放在一个Lambda中。
-
对于Lambda而言,一行是最理想的,三行是合理的极限,如果违背这个原则,可能对程序的可读性造成严重的危害。
-
在Lambda中,this指的是外围实例,在匿名类中,this指匿名类实例。
-
尽可能不要序列化一个Lambda。
-
千万不要给函数对象使用匿名类,除非必须创建非函数接口的类型的实例。
//匿名类 List<String> aStrings = new ArrayList<String>(); Collections.sort(aStrings, new Comparator<String>() { @Override public int compare(String o1, String o2) { return Integer.compare(o1.length(), o2.length()); } }); //Lambda表达式 Collections.sort(aStrings, (s1, s2) -> Integer.compare(s1.length(), s2.length())); //Lambda表达式代替比较器构造方法 Collections.sort(aStrings, Comparator.comparingInt(String::length)); //List中的sort方法 aStrings.sort(Comparator.comparingInt(String::length));
第43条 方法引用优先于Lambda
-
只要方法引用更加简洁,清晰,就用方法应用;如果方法引用并不简洁,就坚持使用Lambda
//lambdamap.merge(key, 1, (count, incr) -> count + incr);//方法引用map.merge(key, 1, Integer::sum);//方法引用sevice.execute(GoshThisClassNameIsHumongous::action);//lambdaservice.execute(() -> action());
第44条 坚持使用标准的函数接口
-
java.util.function
-
只要标准的函数接口能够满足需求,通常应该优先考虑,而不是专门再构建一个新的函数接口。
-
6个基础接口
-
UnaryOperator
-
Operator接口代表其结果与参数类型一致的函数
-
Predicate接口代表带有一个参数并返回一个boolean的函数
-
Function接口代表其参数与返回类型不一致的函数
-
Supplier接口代表没有参数并返回(或提供)一个值的函数
-
Consumer代表的是一个函数但不返回任何值的函数,相当于消费掉了其参数。
| 接口 | 函数签名 | 范例 |
|---|---|---|
| UnaryOperator<T> | T apply(T t) | String::toLowerCase |
| BinaryOperator<T> | T apply(T t1, T t2) | BigInteger::add |
| Predicate<T> | boolean test(T t) | Collection::isEmpty |
| Function<T, R> | R apply(T t) | Arrays::asList |
| Supplier<T> | T get() | Instant::now |
| Consumer<T> | void accept(T t) | System.out::println |
-
千万不要用带包装类型的基础函数接口来代替基本函数接口。(使用装箱基本类型进行批量操作处理,最终会导致致命的性能问题)
-
如果你所需要的函数接口具有一项或者多项以下特征,则必须认真考虑自己编写专用的函数接口,而不是使用标准的函数接口:
-
通用,并且将受益于描述性的名称
-
具有与其关联的严格的契约
-
将受益于定制的缺省方法
-
必须始终用@FunctionInterface注解对自己编写的函数接口进行标注。
-
总而言之,既然Java有了Lambda,就必须时刻谨记用Lambda来设计API。
第45条 谨慎使用Stream
-
Stream中的数据元素可以是对象引用,或者基本类型值。它支持三种基本类型:int,long,double。
-
一个Stream pipeline中包含一个源Stream,接着是0个或多个中间操作和一个终止操作。
-
每个中间操作都会通过某种方式对Stream进行转换,例如将每个元素映射到该元素的函数,或者过滤掉不满足某些条件的所有元素。所有的中间操作都是将一个Stream转换成另一个Stream,其元素类型可能与输入的Stream一样,也可能不同。
-
终止操作会在最后一个中间操作产生的Stream上执行一个最终的计算,例如将其元素保存到一个集合中,并返回某一个元素,或者打印出所有元素等。
-
Stream pipeline是lazy的:对于完成终止操作不需要的数据元素,将永远都不会被计算
-
Stream API是流式的:所有包含pipeline的调用可以链接成一个表达式;多个pipeline也可以链接在一起成为一个表达式。
-
要使pipeline并发执行,只需在该pipeline的任何stream上调用parallel方法即可,但是通常不建议这么做。
-
滥用Stream会使得程序代码更难以读懂和维护。
-
在没有显示类型的情况下,仔细命名Lambda参数,这对于Stream pipeline的可读性至关重要。
-
最好避免利用Stream来处理char值。
-
重构现有代码来使用Stream,并且只在必要的时候才在新代码中使用。
-
不适合Stream的方法:
-
可以读取或修改范围内的任意局部变量
-
可以从外围方法中return,break或continue外围循环
-
Stream可以使完成这些工作变得易如反掌:
-
统一转换元素的序列
-
过滤元素的序列
-
利用单个操作(如添加、连接或者计算其最小值)合并元素的顺序
-
将元素的序列存放到一个集合中,比如根据某些公共属性进行分组
-
搜索满足某些条件的元素的序列
-
如果实在不确定用Stream还是用迭代比较好,那么就两种都试试,看看哪一种更好用吧。
第46条 优先选择Stream中无副作用的函数
-
静态导入Collectors的所有成员是惯例也是明智的,因为这样可以提升Stream pipleline的可读性。
import static java.util.stream.Collectors.*;//结果转换为list, Set, Map.collect(toList());.collect(toSet());.collect(toMap(Object::toString, e -> e));//水果名到数量的映射.groupingBy(Fruit::getName, Collectors.counting());
-
collect:将Stream的元素集中到一个真正的Collection中
-
toMap的两参数,三参数,四参数
-
groupingBy 返回收集器以生成映射,根据分类函数将元素分门别类。
-
总而言之,编写Stream pipeline的本质是无副作用的函数对象。这适用于传入Stream及相关对象的所有函数对象。终止操作中的forEach应该只用来报告由Stream执行的计算结果,而不是让它执行计算。为了正确地使用Stream,必须了解收集器。最重要的收集器工厂是toList、 toSet、toMap、groupingBy和joining。
第47条 Stream要优先用Collection作为返回类型
-
java8之前:优先级:Collection > Iterable > 数组(基本类型)
-
对于公共的、返回序列的方法,Collection或者适当的子类型通常是最佳的返回类型。
-
千万别在内存中保存巨大的序列,将它作为集合返回即可。
/** * stream对象转可迭代对象 * @param stream * @return */ public static <E> Iterable<E> iterableOf(Stream<E> stream) { return stream::iterator; } /** * 可迭代对象转stream * @param iterable * @return */ public static <E> Stream<E> streamOf(Iterable<E> iterable) { return StreamSupport.stream(iterable.spliterator(), false); }
第48条 谨慎使用Stream并行
-
如果源头是来自Stream.iterate,或者使用了中间操作的limit,那么并行pipeline也不可能提升性能。这个pipeline必须同时满足这两个条件。
-
千万不要任意地并行Stream pipeline。
-
在Stream上通过并行获得的性能,最好是通过ArrayList、HashMap、HashSet和ConcurrentHashMap实例,数组,int范围和long范围等。(都可以被精确、轻松地分成任意大小的子范围,使并行线程中的分工变得更加轻松;优异的引用局部性)
-
Stream pipeline的终止操作本质上影响并发执行的效率。并行的最佳终止操作是reduction,例如reduce,min,max,count,sum, anyMathc, allMatch, noneMatch。由collect方法执行的操作都是可变的减法,不是并行的最好选择,因为合并集合的成本非常高。
-
并行Stream不仅可能降低性能,包括活性失败,还可能导致结果出错,以及难以预计的行为。
-
在适当的条件下,给Stream pipeline添加parallel调用,确实可以在多处理器核的情况下实现近乎线性的倍增。某些域如机器学习和数据处理,尤其适用于这样的提速。
-
总而言之,尽量不要并行Stream pipeline。