42. Lambda优先于匿名类
Lambda代码比匿名类更加简洁。删除所有Lambda参数的类型,编译器会利用一个称作类型推导的过程,根据上下文推断出这些类型。不要给函数对象使用匿名类,除非必须创建非函数接口的类型的实例。
Lambda的局限:
a. Lambda没有名称和文档;如果一个计算本身不是自描述的,或者超出了几行(一到三行是合理的),那就不要把它放在一个Lambda中。
b. Lambda限于函数接口,不能创建实例,而匿名类可以。
c. Lambda无法获得对自身的引用,而匿名类可以。
43. 方法引用优先于Lambda
方法引用常常比Lambda表达式更加简洁、清晰。建议优先用方法引用,只有当它不够简洁时才用Lambda。
五种方法引用:
方法引用类型 | 范例 | Lambda等式 | 说明 |
静态 | Integer::parseInt | str->Integer.parseInt(str) | 函数对象与被引用方法带有相同的参数 |
有限制 | Instant.now()::isAfter | Instant then = Instant.now(); t->then.isAfter(t) | 类似于静态引用 |
无限制 | String::toLowerCase | str->str.toLowerCase() | 在声明函数前面添加一个参数来指定接收对象。常用于流管道。 |
类构造器 | TreeMap<K,V>::new | ()->new TreeMap<K,V> | 充当工厂对象 |
数组构造器 | int[]::new | len->new int[len] | 充当工厂对象 |
44. 坚持使用标准的函数接口
44.1 只要标准的函数接口能够满足需求,应该优先考虑,而不是专门再构建一个新的函数接口。
6个基础接口:
接口 | 函数签名 | 范例 |
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 |
44.2 千万不要用带包装类型的基础函数接口来代替基本函数接口。使用装箱基本类型进行批量操作处理,最终会导致致命的性能问题。
44.3 函数接口具有以下一个或多个特征,需要自己编写。记住它是接口,应谨慎设计。
a. 通用,并且将受益于表述性的名称
b. 具有与其关联的严格的契约
c. 将受益于定制的缺省方法
必须始终用@FunctionalInterface注解对自己编写的函数接口进行标注。另外不要在相同的参数位置,提供不同的函数接口来进行多次重载(可能导致歧义)。
44.4 注解@FunctionalInterface的作用:
a. 告诉他人这个接口是针对Lambda设计的
b. 这个接口不会进行编译,除非它只有一个抽象方法
c. 避免后续维护人员不小心给该接口添加抽象方法
45. 谨慎使用Stream
Stream API简化了串行或并行的大批量操作。但是滥用Stream会使程序代码难以读懂和维护。
45.1 使用Stream注意事项
a. 不能忘记终止操作,否则流管道(Stream pipeline)将是一个静默的无操作指令。
b. 在没有显式类型的情况下,仔细命名Lambda参数,对流管道的可读性至关重要。
c. 在流管道中使用helper方法,有利于增强可读性。
d. 最好避免利用Stream来处理char值(可能变int值) 。
e. 重构现有的代码来使用Stream,只在必要是时候才在新代码中使用。
45.2 适合Stream完成的工作
流管道利用函数对象(Lambda或方法引用)来描述重复的计算。
a. 统一转换元素的序列
b. 过滤元素的序列
c. 利用单个操作(如添加、连接或者计算其最小值)合并元素的顺序
d. 搜索满足某些条件的元素的序列
45.3 适合迭代代码完成的工作
a. 从代码块中,可以读取或者修改范围内的任意局部变量;而Lambda不能修改任何局部变量,只能读取final或有效的final变量
b. 从代码块中,可以从外围方法中return、break或continue外围循环,或者抛出该方法声明要抛出的任何受检异常;而Lambda无法完成这些事情。
45.4 如果不能确定用Stream还是用迭代比较好,那么两种都试试,看哪种更好用。许多任务是结合使用两种方法来一起完成的。
46. 优先选择Stream中无副作用的函数
Stream范型最重要的部分是把计算构造成一系列变型,每一级结果都尽可能靠近上一级结果的纯函数。纯函数是指结果只取决于输入的函数:它不依赖任何可变的状态,也不更新任何状态。
foreach操作应该只用于报告Stream计算的结果,而不是执行计算。
静态导入Collectors的所有成员是明智的,可以提升流管道的可读性。最重要的收集器工厂是toList、toSet、toMap、groupingBy和joining,必须了解它们。
47. Stream要优先用Collection作为返回类型
Collection接口是Iterable的一个子类型,它有一个stream方法,提供了迭代和stream访问。对于公共的、返回序列的方法,Collection或者适当的子类型通常是最佳的返回类型。
数组也通过Arrays.asList和Stream.of方法提供了简单的迭代和Stream访问。如果返回的序列足够小,容易存储,或许最好返回标准的集合实现,如ArrayList或者HashSet。但是千万别在内存中保存巨大的序列,将它作为集合返回即可。
在编写返回一系列元素的方法时,记住优先返回集合,如果无法返回集合,就返回Stream或者Iterable,方便使用即可。
48. 谨慎使用Stream并行
并行Stream不仅可能降低性能,包括活性失败,还可能导致结果出错,以及难于预计的行为。
在Stream上通过并行获得性能,最好是通过ArrayList、HashMap、HashSet和ConcurrentHashMap实例,数组,int范围和long范围等。这些数据结构都可以被精确、轻松地分成任意大小的子范围,使并行线程中的分工变得更轻松;另外它们提供了优异的引用局部性:序列化的元素一起保存在内存中。
尽量不要并行Stream,除非有足够的理由相信它能保证计算的正确性,并且加快程序的运行速度。并发运行时一定要确保代码正确,并在真实环境下认真地进行性能测试。