《Effective Java》-Lambda和Stream

157 阅读6分钟

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(); 
}