《Effective Java》阅读笔记-第七章
Effective Java 阅读笔记
第七章 Lambda 和 Stream
第 42 条 Lambda 优先于匿名类
匿名类过于繁琐,使用 Lambda 可以使代码更清晰、更轻量。
但是,Lambda 没有名称和文档,如果一个实现过于复杂,那就不要放在 Lambda 中。
并且,Lambda 中不能获取函数本身的引用,Lambda 中的 this 获取的是外围 this,如果需要获取那就用匿名内部类。
尽可能不要(除非迫不得已)序列化一个 Lambda,如果有需要,应该使用私有静态嵌套类(即private static class
)。
第 43 条 方法引用优先于 Lambda
方法引用也是在 Java 8 中添加进来的,用::
表示,比如引用System.out.println()
方法,就是这样:System.out::println
方法引用比 Lambda 更简洁,也能消除更多的样板代码,同时可读性也更好。
比如:
map.merge(key, 1, (count, incr) -> count + incr);
这个就可以使用方法引用来替换:
map.merge(key, 1, Integer::sum);
这样既可以消除重复变量 count、incr,可读性也更好。
大部分情况下都可以使用方法引用来代替 Lambda 方法,但是也有例外,比如在一个类名很长的类中,写方法实现时:
service.execute(GoshThisClassNameIsHumongus::action);
并不比下面这种好:
service.execute(() -> action());
类似的还有Function.identity()
并不比x -> x
更简洁,也没有更清晰,此时就可以考虑使用x -> x
这种。
简而言之,如果方法引用更简洁、清晰,那就用方法引用,如果方法引用并不简洁,那就用 Lambda。
第 44 条 坚持使用标准的函数接口
Java 8 中加入函数式之后,java.util.function 包中加入了大量预定义的函数接口,不需要记住每个函数,只需要记住常用的 6 个就行:
接口 | 函数签名 | 例子 |
---|---|---|
UnaryOperator | T apply(T t) | String::toLowerCase |
BinaaryOperator | T apply(T t1, T t2) | BigInteger::add |
Predicate | boolean test(T t) | Collections::isEmpty |
Function<T, R> | R apply(T t) | Array::asList |
Supplier | T get() | Instant::new |
Consumer | void accept(T t) | System.out::println |
这 6 个接口还各自有 3 中变体,分别对应 int、long、double,命名方式是在原本的名称前面加上类型,比如IntPredicate<T>
Function 有 9 中变体,分别对应参数和返回值为 int、long、double 类型,命名方式是入参类型To返回类型
,比如IntToLongFunction
,如果结果是对象那就是ToObj
,比如IntToObjFunction
三种更基础的类型Function
、Consumer
、Predicate
还有两个参数的变体,以及两个单数对应基础类型的变体,例如:
BiFunction<T, U, R>
BiConsumer<T, U>
BiPredicate<T, U>
对应的基础类型命名方式如下:
ToIntBiFunction<T, U>
ObjIntConsumer<T>
然后还有BooleanSupplier
,总计 43 个接口。
PS:真鸡儿个复杂,Java这里点处理的不是很好,基础类型、参数数量和类型、返回值类型掺杂在一块,超级混乱。
C#这一点处理的还算可以,但是也不是完美的,函数分裂成Func<T>
和Action<T>
,因为void不能作为泛型。
在使用时,如果可以使用基本类型的接口,那就不要用包装类型的接口,因为会带来很大的性能问题。
当满足以下条件时就可以考虑自己定义函数式接口,而不是使用内置的:
- 比较通用,并且名称可以很好的说明这个接口的作用时
- 有比较严格的约定时
- 需要特殊默认方法时
而且自己生命函数式接口时,需要用@FunctionalInterface
来标记,这个注解和@Override
类似。
第 45 条 使用 Stream 时需谨慎
Stream 配合函数式接口写起来很爽,但是滥用会让代码更难读、难维护。
在没有显式类型的情况下,仔细命名 lambda 的参数会让 Stream Pipeline 可读性更好。
Java 中没有 charStream,char 在 Stream 中会变成 int,因此最好不好用 Stream 处理char。
部分情况只能通过普通循环来完成,不能使用 Stream 处理:
- 读取或修改局部变量时:Stream 只能读取 final 变量,并且也不能修改变量(虽然可以通过传入 final 的数组来实现,但是并不建议)。
- 需要 return、break、continue,或者抛出受检异常时
下面这些情况用 Stream 非常好处理:
- 统一转换元素列表
- 过滤元素列表
- 合并元素序列(最大值、最小值、求和时)
- 元素列表分组
- 搜索某些条件的元素
第 46 条 优先选择 Stream 中无副作用的函数
避免在 forEach 函数中操作外部数据,如果可以的话,使用 collect 方法去收集,而不是在 forEach 中去一个一个 add。
第 47 条 Stream 优先用 Collection 作为返回类型
这个意思是用 Collection 或者其子类型作为返回值更合适,不要返回 Iterable。
第 48 条 使用 Stream 并行应谨慎
任意使用parallel()
方法不仅可能降低性能,还有可能调用失败甚至结果出错。
适当的情况下使用并行流可以提高效率。一般来说,如果每个元素互不干扰,那么并行流就可以提高效率。但是有状态的时候就很有可能出错,比如在 reduce 时。