Lambda和Stream
Item 42: Prefer lambdas to anonymous classes
Lambda优先于匿名类
具有单个抽象方法的接口称为函数类型,其实例称为函数对象。创建函数对象的主要方式就是匿名类(anonymous class)。
Java 8中,,具有单个抽象方法的接口称为函数接口,并允许使用Lambda表达式来创建函数式接口的实例。使用Lambda表达式之后,代码变得简洁很多。
然而,Lambda表达式也是有一定的局限性。
- 其一,没有名称和文档
- 其二,如果超过三行,则不要使用Lambda表达式。较长的Lambda表达式反而降低了可读性
- 其三,Lambda无法获取自身的引用,
this关键字指的的外围实例。而在匿名类中,this指的是匿名类实例
总之,不要给函数对象使用匿名类,除非必须创建非函数接口的类型的实例。
Item 43: Prefer method references to lambdas
方法引用优先于Lambda表达式
使用方法引用通常能得到更精简、清晰的代码。然而,通常情况下,使用方法引用能够做到的事情,使用Lambda表达式也能做到。有的时候,方法引用的可读性和精简性并不如Lambda表达式。
许多方法引用都指向静态方法,但是也有其他的四种情况:
- 有限制:
Instant.now() :: isAfter; - 无限制:
String :: toLowerCase - 类构造器:
TreeMap<K, V> :: new - 数组构造器:
int[] :: new
总而言之,如果方法引用更加简洁,则使用方法引用;反之,坚持使用Lambda表达式。
Item 44: Favor the use of standard functional interfaces
优先使用标准的函数式接口
Java中6种基本的函数接口概述如下:
| 接口 | 函数签名 | 范例 |
|---|---|---|
UnaryOperator<T> | T apply (T t) | String::toLowerCase |
自己的编写的函数式接口应当具有如下的特征:
- 通用,并且将受益于描述性的信息
- 具有与其关联的严格的契约
- 将受益于定制的缺省方法
Item 45: Use streams judiciously
谨慎地使用Stream
Java 8中增加了Stream API,这个API提供了两个重要的抽象:
- Stream:代表数据元素有限或者无限的顺序
- Stream pipeline:元素的多级运算
Stream pipeline由三部分组成:
- 源Stream
- 中间操作:对流中元素进行的操作
- 终止操作:对中间操作产生的流执行最终的计算,例如保存到集合中等
注意,Stream pipeline是lazy的,直到调用终止操作时才开始计算,没有终止操作的流是一个没有任何操作的静默流。
关于Stream中常用的API,将会另外总结为一篇文章。
在没有任何显式类型的情况下,仔细命名Lambda参数对于Stream pipeline的可读性十分重要。
另外,在对char数组使用Stream流的时候,一定要注意对结果进行强制类型转换,否则得到的都是int类型的数字。不过,最好不要使用Stream来处理char值。
何时最好使用Stream呢?
- 统一转换元素的序列
- 过滤元素的序列
- 利用单个操作合并元素的顺序
- 将元素的序列放到一个集合中
- 搜索满足某些条件的元素的序列
Item 46: Prefer side-effect-free funcitons in streams
优先选择Stream中无副作用的函数
Stream泛型最重要的部分就是把计算构造成一系列变型,每一级结果都尽可能靠近上一级结果的纯函数。所谓纯函数,就是指输出直接和输入有关,而不取决于任何状态。
对于forEach,其操作应该只用于报告Stream计算的结果,例如打印或者添加到已经存在的集合中,而不是用于计算。
将计算结果的Stream元素集中到一个Collection容器中需要使用到三种收集器:toList() | toSet() | toCollection(collectionFactory),分别返回一个列表、一个集合和一个制定的集合类型。
映射收集器toMap
Collectors.toMap()用于将Stream中的元素映射为一个Map,该方法有多个重载方法:
(1)两个函数参数
toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends V> valueMapper)
keyMapper: 从流中提取键的函数valueMapper: 从流中提取值的函数
注意:键冲突的时候会抛出IllegalStateException
(2)三个函数参数
toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends V> valueMapper,
BinaryOperator<V> mergeFunction)
BinaryOperator<V> mergeFunction): 键冲突的时候,用于合并值的函数
注意:
map中的键和值都不允许为空,否则会抛出NPE
分组函数gorupingby
groupingBy()函数常用于collect()函数中,用于将元素按照某些分类规则分组。下面是此函数的一个基本用法:
Map<K, List<T>> groupedResult = stream.collect(Collectors.groupingBy(classifierFunction));
groupingBy函数还有更高级的用法,传递第二个函数为下游收集器,对每个分组执行进一步的收集操作,例如
Map<Integer, Long> countByAge = people.stream() .collect(Collectors.groupingBy(Person::getAge, Collectors.counting()));
连接函数joining
Collectors.joining()函数用于连接字符串的终端操作,将字符连接为一个单一的字符串。该方法有三个重载:
(1)基本用法
String result = words.stream().collect(Collectors.joining());
(2)带分隔符
String result = words.stream().collect(Collectors.joining(", "));
(3)带有前缀后缀
// 字符串前缀为 [ ,后缀为 ] ,并且每个元素之间的分隔符为 ,
String result = words.stream().collect(Collectors.joining(", ", "[", "]"));
需要注意的是,该方法仅仅用于处理Stream<String>,如果有其他类型的流,例如Stream<Char>,需要将其映射为String类型的流,才能进行处理。
Item 47: Prefer Collection to Stream as a return type
优先使用Collection而非Stream作为返回结果
在编写返回一些列元素的方法的时候,如果可以返回集合,那就返回集合,如果无法返回一个结合,那就返回一个Stream或者Iterable。
Item 48: Use caution when making streams parallel
谨慎地使用并行Stream
Stream.iterate
Stream.iterate用于创建一个无限顺序流,可以根据逻辑生成一系列的值,具有两种重在方式:
(1)无限流
Stream.iterate(T seed, UnaryOperator<T> f)
seed: 序列的初始值f: 用于生成流的下一个元素的函数
(2)带限制的无限流
Stream.iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next)
hasNext: 检查是否需要生成下一个元素next: 生成下一个元素的函数
如果流的源头是一个Stream.iterate或者使用了中间操作limit,那么并行的pipeline也不会提升性能。
如果想要利用parallel stream的优点,最好是通过ArrayList | HashMap | HashSet | ConcurrentHashMap实例,数组、int范围或者long范围等。他们的共同特点就是可以拆分为任意大小的子范围,这样就可以让并行线程的分工变得更加轻松。
所有的并行的Stream pipeline都是在同一个fork-join池中运行的,只要有一个pipeline发生异常,那么就会影响其他部分的性能。
一个比较好地利用并行流的例子,用于计算小于n的素数的个数。
static long pi(long n) {
return LongStream.rangeClosed(2, n)
.parallel()
.mapToObj(BigInteger::valueOf)
.filter(i -> i.isProbablePrime(50))
.count();
}