Effective Java 3nd 笔记-第七章 Lambda和Stream

222 阅读7分钟
原文链接: mp.weixin.qq.com

第七章 Lambda和Stream

第42条 Lambda优先于匿名类

  • 在Java8中,形成了“带有单个抽象方法的接口是特殊的,值得特殊对待”的观念。这些接口现在被称作函数接口,Java允许利用Lambda表达式创建这些接口的实例。

  • 删除所有Lambda参数的类型吧,除非它们的存在能够使程序变得更加清晰

  • 与方法和类不同的是,Lambda没有名称和文档;如果一个计算本身不是自描述的,或者超出了几行,那就不要把它放在一个Lambda中。

  • 对于Lambda而言,一行是最理想的,三行是合理的极限,如果违背这个原则,可能对程序的可读性造成严重的危害。

  • 在Lambda中,this指的是外围实例,在匿名类中,this指匿名类实例。

  • 尽可能不要序列化一个Lambda。

  • 千万不要给函数对象使用匿名类,除非必须创建非函数接口的类型的实例。

		//匿名类		List<String> aStrings = new ArrayList<String>();		Collections.sort(aStrings, new Comparator<String>() {			@Override			public int compare(String o1, String o2) {				return Integer.compare(o1.length(), o2.length());			}		});		//Lambda表达式		Collections.sort(aStrings, (s1, s2) -> Integer.compare(s1.length(), s2.length()));		//Lambda表达式代替比较器构造方法		Collections.sort(aStrings, Comparator.comparingInt(String::length));		//List中的sort方法		aStrings.sort(Comparator.comparingInt(String::length));

第43条 方法引用优先于Lambda

  • 只要方法引用更加简洁,清晰,就用方法应用;如果方法引用并不简洁,就坚持使用Lambda

//lambdamap.merge(key, 1, (count, incr) -> count + incr);//方法引用map.merge(key, 1, Integer::sum);//方法引用sevice.execute(GoshThisClassNameIsHumongous::action);//lambdaservice.execute(() -> action());

第44条 坚持使用标准的函数接口

  • java.util.function

  • 只要标准的函数接口能够满足需求,通常应该优先考虑,而不是专门再构建一个新的函数接口。

  • 6个基础接口

    • UnaryOperator

    • Operator接口代表其结果与参数类型一致的函数

    • Predicate接口代表带有一个参数并返回一个boolean的函数

    • Function接口代表其参数与返回类型不一致的函数

    • Supplier接口代表没有参数并返回(或提供)一个值的函数

    • Consumer代表的是一个函数但不返回任何值的函数,相当于消费掉了其参数。

接口 函数签名 范例
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
  • 千万不要用带包装类型的基础函数接口来代替基本函数接口。(使用装箱基本类型进行批量操作处理,最终会导致致命的性能问题)

  • 如果你所需要的函数接口具有一项或者多项以下特征,则必须认真考虑自己编写专用的函数接口,而不是使用标准的函数接口:

    • 通用,并且将受益于描述性的名称

    • 具有与其关联的严格的契约

    • 将受益于定制的缺省方法

  • 必须始终用@FunctionInterface注解对自己编写的函数接口进行标注。

  • 总而言之,既然Java有了Lambda,就必须时刻谨记用Lambda来设计API。

第45条 谨慎使用Stream

  • Stream中的数据元素可以是对象引用,或者基本类型值。它支持三种基本类型:int,long,double。

  • 一个Stream pipeline中包含一个源Stream,接着是0个或多个中间操作和一个终止操作。

    • 每个中间操作都会通过某种方式对Stream进行转换,例如将每个元素映射到该元素的函数,或者过滤掉不满足某些条件的所有元素。所有的中间操作都是将一个Stream转换成另一个Stream,其元素类型可能与输入的Stream一样,也可能不同。

    • 终止操作会在最后一个中间操作产生的Stream上执行一个最终的计算,例如将其元素保存到一个集合中,并返回某一个元素,或者打印出所有元素等。

    • Stream pipeline是lazy的:对于完成终止操作不需要的数据元素,将永远都不会被计算

    • Stream API是流式的:所有包含pipeline的调用可以链接成一个表达式;多个pipeline也可以链接在一起成为一个表达式。

    • 要使pipeline并发执行,只需在该pipeline的任何stream上调用parallel方法即可,但是通常不建议这么做。

  • 滥用Stream会使得程序代码更难以读懂和维护。

  • 在没有显示类型的情况下,仔细命名Lambda参数,这对于Stream pipeline的可读性至关重要。

  • 最好避免利用Stream来处理char值。

  • 重构现有代码来使用Stream,并且只在必要的时候才在新代码中使用。

  • 不适合Stream的方法:

    • 可以读取或修改范围内的任意局部变量

    • 可以从外围方法中return,break或continue外围循环

  • Stream可以使完成这些工作变得易如反掌:

    • 统一转换元素的序列

    • 过滤元素的序列

    • 利用单个操作(如添加、连接或者计算其最小值)合并元素的顺序

    • 将元素的序列存放到一个集合中,比如根据某些公共属性进行分组

    • 搜索满足某些条件的元素的序列

  • 如果实在不确定用Stream还是用迭代比较好,那么就两种都试试,看看哪一种更好用吧。

第46条 优先选择Stream中无副作用的函数

  • 静态导入Collectors的所有成员是惯例也是明智的,因为这样可以提升Stream pipleline的可读性。

import static java.util.stream.Collectors.*;//结果转换为list, Set, Map.collect(toList());.collect(toSet());.collect(toMap(Object::toString, e -> e));//水果名到数量的映射.groupingBy(Fruit::getName, Collectors.counting());
  • collect:将Stream的元素集中到一个真正的Collection中

  • toMap的两参数,三参数,四参数

  • groupingBy 返回收集器以生成映射,根据分类函数将元素分门别类。

  • 总而言之,编写Stream pipeline的本质是无副作用的函数对象。这适用于传入Stream及相关对象的所有函数对象。终止操作中的forEach应该只用来报告由Stream执行的计算结果,而不是让它执行计算。为了正确地使用Stream,必须了解收集器。最重要的收集器工厂是toList、 toSet、toMap、groupingBy和joining。

第47条 Stream要优先用Collection作为返回类型

  • java8之前:优先级:Collection > Iterable > 数组(基本类型)

  • 对于公共的、返回序列的方法,Collection或者适当的子类型通常是最佳的返回类型。

  • 千万别在内存中保存巨大的序列,将它作为集合返回即可。

/**	 * stream对象转可迭代对象	 * @param stream	 * @return	 */	public static <E> Iterable<E> iterableOf(Stream<E> stream) {		return stream::iterator;	}	/**	 * 	可迭代对象转stream	 * @param iterable	 * @return	 */	public static <E> Stream<E> streamOf(Iterable<E> iterable) {		return StreamSupport.stream(iterable.spliterator(), false);	}

第48条 谨慎使用Stream并行

  • 如果源头是来自Stream.iterate,或者使用了中间操作的limit,那么并行pipeline也不可能提升性能。这个pipeline必须同时满足这两个条件。

  • 千万不要任意地并行Stream pipeline。

  • 在Stream上通过并行获得的性能,最好是通过ArrayList、HashMap、HashSet和ConcurrentHashMap实例,数组,int范围和long范围等。(都可以被精确、轻松地分成任意大小的子范围,使并行线程中的分工变得更加轻松;优异的引用局部性)

  • Stream pipeline的终止操作本质上影响并发执行的效率。并行的最佳终止操作是reduction,例如reduce,min,max,count,sum, anyMathc, allMatch, noneMatch。由collect方法执行的操作都是可变的减法,不是并行的最好选择,因为合并集合的成本非常高。

  • 并行Stream不仅可能降低性能,包括活性失败,还可能导致结果出错,以及难以预计的行为。

  • 在适当的条件下,给Stream pipeline添加parallel调用,确实可以在多处理器核的情况下实现近乎线性的倍增。某些域如机器学习和数据处理,尤其适用于这样的提速。

  • 总而言之,尽量不要并行Stream pipeline。