目录
Item 42: Prefer lambdas to anonymous classes
Item 43: Prefer method references to lambdas
Item 44: Favor the use of standard functional interfaces
Item 45: Use steams judiciously
Item 46: Prefer side-effect-free functions in streams
Item 47: Prefer Collection to Stream as a return type
Item 48: Use caution when making streams parallel
Item 42: Prefer lambdas to anonymous classes
Lambda优先于匿名类
术语:
- function types:函数类型
- function objects:函数对象
- anonymous class:匿名类
- functional interfaces:函数接口,只包含一个抽象方法的接口(非抽象方法可以有多个)。定义函数接口时,推荐加注解@FunctionInterface,帮助检查一些错误(也可不加)。
- Lambda expressions:Lambda表达式,简称Lambda,用来创建函数接口的实例。
- type inference:类型推断
Lambdas会自动进行类似推断,除非无法推断,否则建议不用写入参或返回参数类型。
Lambda不适用场景:
计算过程比较复杂、代码多行(1行理想、3为上限)。
Lambda VS anonymous class:
- 优先Lambda,用不了再考虑匿名类。
- Lambda内部的this指向外围实例,匿名类的this指向匿名类自身实例。
- 需要序列或反序列化的场景,不要用Lambda和匿名类(无法序列和反序列化类的性质),而是用私有嵌套类。
Item 43: Prefer method references to lambdas
方法引用优先于Lambda(前提:方法引用能更简洁时)
术语:
- method references:方法引用,用法:类名::方法名,或者 类实例::方法名,调用处的函数入参和返回类型需和方法引用的要一致。(可参考 Java 8 方法引用 | 菜鸟教程 )
方法引用的5种类型:
方法引用类型 | 范例 | Lambda等式 |
静态(Static) | Integer::parseInt | str->Integer.parseInt(str) |
有限制(Bound) | Instant.now()::isAfter | Instant then=Instant.now(); t->then.isAfter(t) |
无限制(Unbound) | String::toLowerCase | str->str.toLowerCase() |
类构造器 | TreeMap<K,V>::new | ()->new TreeMap<K,V> |
数组构造器 | int[]:new | ()->new int[len] |
方法引用 VS Lambda:
- Lambda可读性和维护性更强。
- 方法引用总体而言更简洁(有时不是)。
- 方法引用能做的事,Lambda也可实现(严格讲,有个例外)。
Item 44: Favor the use of standard functional interfaces
坚持使用标准的函数接口
标准的函数接口在包java.util.function中,如断言接口Predicate,应优先使用里面,而非专门构建。标准的函数接口有43个,但基础接口有6个,其余的可根据这6个基础接口推断出:
函数接口 | 函数签名 | 范例 | 说明 |
UnaryOperator<T> | T apply(T t) | String::toLowerCase | 参数和返回类型一致 |
BinaryOperator<T> | T apply(T t1, T t2) | BigInteger::add | 参数有2个 |
Predicate<T> | boolean test(T t) | Collection::isEmpty | 有参数,返回boolen |
Function<T> | R apply(T t) | Arrays::asList | 参数和返回类型不同 |
Supplier<T> | T get() | Instant::now | 无参数,有返回值 |
Comsumer<T> | void accept(T t) | System.out::println | 有参数,无返回值 |
现在倾向于用函数接口替代模板方法模式,即通过提供一个函数对象给静态工厂或类构造器来实现相同的功能。
注意:
- 大多数标准函数接口支持基本类型,虽然也可用封装类型,但不建议,因为容易出现性能问题。
- 不要在调用函数接口的方法出现重载情况,因为容易导致客户端出现歧义。
Item 45: Use steams judiciously
谨慎使用Stream
术语:
- stream:流,代表数据元素有限或无限序列。
- steam pipeline:流管道,一个管道代表数据元素的一个多级计算。包含1个源stream、0或多个中间操作(intermediate operation)和1个终止操作(terminal operation),具体操作可查看java.util.stream.Stream包,每各方法的注释会标注操作类别。
流管道是懒惰(lazy)计算,直到调用终止操作时才会执行中间操作。
stream应该避免滥用,在保证可读性和维护性的前提下尽可能使用。
建议使用stream的场景:转换、过滤、搜索、分组、对元素间作某种计算(如加)。
flatMap()用于将每个stream的元素平铺,最后合并成一个新的stream。比如,[[1,2],[3,4]]中含2个数组,flatMap()可将内部数组[1,2]平铺为1,2,最后构成新的数组[1,2,3,4]。
List<List<Integer>> llInt = new ArrayList<>(); // 用于存储:[[1,2],[3,4]]
llInt.add(Arrays.asList(1, 2));
llInt.add(Arrays.asList(3, 4));
llInt.stream().flatMap(Collection::stream).forEach(System.out::print); // 打印:1234
Item 46: Prefer side-effect-free functions in streams
优先选择Stream中无副作用的函数
术语:
pure function:纯函数,函数结果只取决于输入的函数,不改变也不依赖任何状态。
downstream collector:下游收集器,用于将stream中的所有元素转换为一个值的方法,如counting()用于计算stream中的元素个数。
stream中的每级操作建议调用纯函数(pure function),如:
// 不推荐用法(虽然结果是对的),非纯函数,每次迭代都依赖freq上一次的状态,且会改变freq的状态
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
}
// 推荐用法,
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words.collect(groupingBy(String::toLowerCase (), counting()));
}
stream的终端操作forEach()只应该用在报告由stream计算的结果(或偶尔用于其他目的),而不是用来迭代计算。
收集器(collector)中提供了许多供stream使用的集合/Map类方法(Collectors API)。
Item 47: Prefer Collection to Stream as a return type
Stream要优先用Collection作为返回类型
返回序列的类型主要有4种:Collection(List、Set等)、Iterable、数组、Stream。
- 1 若明确返回序列的使用场景类型,则返回对应的类型;
- 2-1 若不明确序列的返回类型,在不用考虑性能的前提下,优先选择Collection作为返回类型,因为Collection继承了Iterable且有Stream方法,可满足Iterable和Stream的使用场景;
- 2-2 若不明确序列的返回类型,当需要考虑性能时(序列很大),则应该使用数组或者专门构建的集合(如幂集power set、连续子集),可以通过Arrays.asList和Stream.of方法将数组转换为Collection和Stream类型。
Item 48: Use caution when making streams parallel
谨慎使用Stream并行
如果stream的源source使用了Stream.iterate或者中间操作使用了limit(),则不要使用并行。
适合stream并行的场景:终端操作terminal operations适合使用归并类reduction函数(和大数据中的map-reduce中的reduce一个含义),如reduce()、min()、max()、count()、sumdeng()等。
即使某个场景各条件已经适合stream并行操作,stream处理的元素数量要达到代码行数的十几万倍,才能抵消并行相关的成本。
开启并行方法:在stream源和中间操作间加上“.paraller()”,或者直接用parallelStream()代替stream(),将一个串行流转换成并行流:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sum = numbers.stream().reduce(0, Integer::sum);
int sumP1 = numbers.stream().parallel().reduce(0, Integer::sum); // 在source后加.parallel()
int sumP2 = numbers.parallelStream().reduce(0, Integer::sum); // 将stream()替换成parallelStream()
System.out.println("串行计算:" + sum + ";并行计算1:" + sumP1 + ";并行计算2:" + sumP2);
// 打印:串行计算:55;并行计算1:55;并行计算2:55