第42条 Lambda优先于匿名类
建议
- 删除所有lambda参数的类型,除非其存在能够使程序更加清晰。
- 与方法和类不同,Lambda没有名称和文档,如果一个计算本身不是不言自明的,或者该计算超出了三行的限制,那么就不要把它放入一个Lambda中。
- 尽可能不要序列化一个Lambda或者匿名内部类实例。
- 不要为函数式接口或者函数式抽象类去创建匿名内部类对象。
函数对象
函数对象:只带有单个抽象方法的接口或者抽象类,其实例称之为函数对象。
Java中增加了Lambda后,许多地方可以使用函数对象了,如下第34条的Operation枚举类型,可以从旧版本优化为新版本:
// Enum type with constant-specific class bodies & data (Item 34)
public enum Operation {
PLUS("+") {
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
public double apply(double x, double y) {
return x - y;
}
},
TIMES("*") {
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE("/") {
public double apply(double x, double y) {
return x / y;
}
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
public abstract double apply(double x, double y);
}
使用Lambda后的版本
// Enum with function object fields & constant-specific behavior
public enum Operation {
PLUS("+", (x, y) -> x + y),
MINUS("-", (x, y) -> x - y),
TIMES("*", (x, y) -> x * y),
DIVIDE("/", (x, y) -> x / y);
private final String symbol;
private final DoubleBinaryOperator op;
Operation(String symbol, DoubleBinaryOperator op) {
this.symbol = symbol;
this.op = op;
}
@Override
public String toString() {
return symbol;
}
public double apply(double x, double y) {
return op.applyAsDouble(x, y);
}
}
第43条 方法引用优先于Lambda
建议
- 当方法引用更简洁时,使用方法引用;反之,还是使用Lambda。
方法引用分类
方法引用类型 | 范例 | Lambda等式 |
---|---|---|
静态引用 | Integer::parseInt | str -> Integer.parseInt(str) |
有限实例引用 | Instant.now()::isAfter | Instant then = Instant.now(); <br/>t -> then.isAfter(t) |
无限实例引用 | String::toLowerCase | str -> str.toLowerCase() |
类构造器 | TreeMap<K,V>::new | () -> new TreeMap<K,V> |
数组构造器 | int[]::new | len -> new int[len] |
- 有限方法引用和静态引用相似,都是所提供的方法引用与函数式接口的参数列表以及返回值类型一致,有限实例方法引用其实不需要实例对象。
- 无限方法引用,则需要一个特定的实例对象,该实例对象加上引用方法中的参数列表,才与函数式接口的参数列表一致。这个实例对象也就是原书上所说的
an additional parameter
。
注:书中的receiving object
其实指的是根据方法引用所创建的函数对象。
附上Java官网中的分类及其示例:
Kind | Example |
---|---|
Reference to a static method | ContainingClass::staticMethodName |
Reference to an instance method of a particular object | containingObject::instanceMethodName |
Reference to an instance method of an arbitrary object of a particular type | ContainingType::methodName |
Reference to a constructor | ClassName::new |
静态方法引用
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public int getAge() {
// ...
}
public LocalDate getBirthday() {
return birthday;
}
public static int compareByAge(Person a, Person b) {
return a.birthday.compareTo(b.birthday);
}
}
//Person::compareByAge就是静态方法引用
有限实例方法引用
String[] stringArray = { "Barbara", "James", "Mary", "John",
"Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);
无限实例方法引用
class ComparisonProvider {
public int compareByName(Person a, Person b) {
return a.getName().compareTo(b.getName());
}
public int compareByAge(Person a, Person b) {
return a.getBirthday().compareTo(b.getBirthday());
}
}
ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);
构造器方法引用
public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>>
DEST transferElements(
SOURCE sourceCollection,
Supplier<DEST> collectionFactory) {
DEST result = collectionFactory.get();
for (T t : sourceCollection) {
result.add(t);
}
return result;
}
//类构造器方法引用
Set<Person> rosterSet = transferElements(roster, HashSet::new);
//等价于
Set<Person> rosterSet = transferElements(roster, HashSet<Person>::new);
第44条 坚持使用标准的函数接口
建议
- 只要标准的函数接口能够满足需求,通常应优先考虑吗,而不是再专门构建一个新的函数接口。
- 千万不要用带包装类型的基础函数接口来代替基础类型的函数接口
- 必须始终用@FunctionalInterface注解对自己编写的函数接口进行标准
Function API
- 在Java9中,java.util.Function一共有43个接口,其中有6个基础接口:
- 参数类型和返回类型一致的有2个,分别是1个参数和两个参数的;
- 返回类型为boolean的有1个;
- 参数类型和返回类型不一致的有1个;
- 无返回值和无形参的各一个,共2个;
接口 | 函数签名 | 范例 |
---|---|---|
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 |
- 其中每个基础接口各自有3种变体,分别作用于将T的类型替换为基本类型int、long和double,共6*3种。
接口 | Int变种 | Long变种 | Double变种 |
---|---|---|---|
UnaryOperator<T> | IntUnaryOperator | LongUnaryOperator | DoubleUnaryOperator |
BinaryOperator<T> | IntBinaryOperator | LongBinaryOperator | DoubleBinaryOperator |
Predicate<T> | IntPredicate | LongPredicate | DoublePredicate |
Function<T,R> | IntFunction<R> | LongFunction<R> | DoubleFunction<R> |
Supplier<T> | IntSupplier | LongSupplier | DoubleSupplier |
Consumer<T> | IntConsumer | LongConsumer | DoubleConsumer |
- 针对于
Function<T,R>
还有9种变体,其中6种用于源类型和结果类型都是基础类型,但又互不相同的情况,还有三种是将源类型为基础类型,结果类型是引用类型的(即返回了个对象):
Int | Long | Double | Obj | |
---|---|---|---|---|
Int | - | IntToLongFunction | IntToDoubleFunction | IntFunction<R> |
Long | LongToIntFunction | - | LongToDoubleFunction | LongFunction<R> |
Double | DoubleToIntFunction | DoubleToLongFunction | - | DoubleFunction<R> |
Predicate<T>
Function<T,R>
Consumer<T>
还有各自有一个带有两个参数的版本,共3个;BiFunction<T,U,R>
还有返回相关的三个基础类型的变种共3个;Consumer<T>
接口还有带有1个引用类型参数和1个基础类型参数的变种,针对三种基础类型,共3个。
源类型 | 变种1 | 变种2 | 变种3 |
---|---|---|---|
Bi | BiPredicate<T,U> | BiFunction<T,U,R> | BiConsumer<T,U> |
BiFunction<T,U,R> | ToIntBiFunction<T,U> | ToLongBiFunction<T,U> | ToDoubleBiFunction<T,U> |
Consumer<T> | ObjDoubleConsumer<T> | ObjIntConsumer<T> | ObjLongConsumer<T> |
- 最后有
Supplier<T>
的一个变种BooleanSupplier
,如下:
@FunctionalInterface
public interface BooleanSupplier {
/**
* Gets a result.
*
* @return a result
*/
boolean getAsBoolean();
}
第45条 谨慎使用Stream
建议
- 滥用Stream会使程序代码难以读懂和维护
- 在没有显式类型的情况下,仔细命名Lambda参数,这对Stream pipeline的可读性至关重要
- 为了可读性,需要时在Stream Pipline中使用helper方法
- 避免利用Stream处理char值
- 重构现有代码来使用Stream,且只在必要的时候才在新代码中使用
- 不确定迭代和Stream哪种比较好的情况下,两种都试下比较下
stream的定义及抽象原理
在Java8中新增了Stream API,用于简化串行或者并行的大批量操作。这个API提供了两个关键的抽象:
- stream(流),表达了一个有限或者无限的数据元素序列。
- stream pipeline(流管道),表达了这些数据元素的多级计算。
stream的来源包括集合、数组、文件、正则表达式模式匹配器、伪随机数生成器以及其他stream。其中的数据元素类型可以是引用类型和基础类型(int、long、double)。一个stream pipeline中包含了一个源stream ,接着是零至多个中间操作和一个终止操作。每个中间操作都会以某种方式对接收到的stream转换为另一个stream,终止操作会在最后一个中间操作产生的stream上执行一个最终的计算。
stream pipeline通常是lazy的,直到调用终止操作时才会开始计算,对于完成终止操作不需要的数据元素,将永远不会计算。这种计算使得无限stream变为可能。而没有终止操作的stream pipeline将是一个静默的无操作指令,千万不能忘记终止操作。
在默认情况下,stream pipeline是按照顺序执行的,要使其并发执行,只需在该pipeline的任何stream上调用parallel方法即可。但通常不建议这么做。
补充说明
helper方法指的是对pipeline中的复杂计算流程,可以单独封装出来作为一个函数。
public class Anagrams {
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> words = Files.lines(dictionary)) {
words.collect(groupingBy(word -> alphabetize(word)))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.forEach(g -> System.out.println(g.size() + ": " + g));
}
}
//helper方法
private static String alphabetize(String word) {
String s = word.chars().sorted()
.collect(StringBuilder::new,
(sb, c) -> sb.append((char) c),
StringBuilder::append).toString();
return s;
}
}
中间操作flatMap可以将Stream中的每个元素都映射产生一个Stream,然后将这些新的Stream全部合并到一个Stream中。
// Iterative Cartesian product computation
private static List<Card> newDeck() {
List<Card> result = new ArrayList<>();
for (Suit suit : Suit.values())
for (Rank rank : Rank.values())
result.add(new Card(suit, rank));
return result;
}
第46条 优先选择Stream中无副作用的函数
建议
- foreach应该只用于报告Stream计算的结果,而不是执行计算。当然,有时候也用于其他目的,比如将计算的结果加入到之前已经存在的集合当中去。
- 将Collectors的所有成员静态导入(import)到使用类中是明智的惯例,能够提升Stream Pipeline的可读性。
Stream范式
Stream并不是一个API,是一种基于函数编程的模型,为了更好地使用Stream,需要遵循一些范式。
paradigm (范式) = philosophy (理念) + methods (方法)=主流认为什么事该做 + 方式 + 方法源自知乎
Stream范式的最重要的部分是,每一级计算(Stream Pipeline)都要尽可能是纯函数。纯函数指的是函数计算的结果仅取决于输入,不依赖于任何可变的状态,同时也不更新任何状态。为了遵循这一点,传入Stream操作的任何函数对象,无论是中间操作还是终止操作,都应该是无副作用的。
Collectors API(收集器 API)
Collectors API有39种方法,其中一些方法带有五个类型参数。对于初学者,可以忽略Collectors接口,将收集器作为封装reduction策略的一个黑盒对象使用。reduction的意思是将Stream的元素合并到单个对象中去,其产生的对象一般是一个集合。
toList() ,toSet() ,toCollection()
将Stream中的元素集中到一个真正的Collection中是比较简单的,有3个这样的收集器:toList()、toSet()、toCollection(collectionFactory)。
//toList
public static <T> Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
(left, right) -> { left.addAll(right); return left; },
CH_ID);
}
//toSet
public static <T> Collector<T, ?, Set<T>> toSet() {
return new CollectorImpl<>((Supplier<Set<T>>) HashSet::new, Set::add,
(left, right) -> { left.addAll(right); return left; },
CH_UNORDERED_ID);
}
//toCollection(collectionFactory)
public static <T, C extends Collection<T>> Collector<T, ?, C> toCollection(Supplier<C> collectionFactory) {
return new CollectorImpl<>(collectionFactory, Collection<T>::add,
(r1, r2) -> { r1.addAll(r2); return r1; },
CH_ID);
}
toMap
将Stream中的元素收集到map中,有3个版本的方法:
//最简单的版本,当多个Stream元素映射到同一个键,pipeline会抛出一个IllegalStateException异常
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}
//示例
private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(toMap(Object::toString, Function.identity()));
//在上述版本添加了合并函数(merge function)的版本
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction) {
return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}
//示例:保留最后加入的元素的策略
// Collector to impose last-write-wins policy
toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
//在加入合并函数的版本基础上,增加一个map工厂,可以指定特定的map实现,如TreeMap、EnumMap等
public static <T, K, U, M extends Map<K, U>>
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier) {
BiConsumer<M, T> accumulator
= (map, element) -> map.merge(keyMapper.apply(element),
valueMapper.apply(element), mergeFunction);
return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}
//示例
Stream.of(propertyUtils.getPropertyDescriptors(type)).
.collect(toMap(ClassProperty::getName, Function.identity(), (k, k2) -> k, LinkedHashMap::new));
针对这三种版本,还有另外对应的变换形式,即相应的toConcurrentMap,能够有效地并行运行,并生成ConcurrentMap实例。
groupingBy
Collectors API还提供了groupingBy方法,将Stream中的元素,根据分类函数(classifier function)进行分类,最终根据所有类别得到一个map。分类函数以元素为输入,返回一个类别,这个类别就是这个元素在map中的key,value为同一类元素组成的集合。有以下3个版本:
//最简单的版本,只需传入一个分类函数。返回Map<K, List<T>
public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) {
return groupingBy(classifier, toList());
}
//带有下游收集器(downstream collector)的版本,可以指定返回的Map中值的集合,如toSet()
public static <T, K, A, D>
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
Collector<? super T, A, D> downstream) {
return groupingBy(classifier, HashMap::new, downstream);
}
//同时支持指定下游收集器和map工厂。如可以指定一个收集器返回Map<K,Set<K>>
public static <T, K, D, A, M extends Map<K, D>>
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
Supplier<M> mapFactory,
Collector<? super T, A, D> downstream) {
/*...*/
}
对应的有3种对应的变体groupingByConcurrent方法,可以有效地并发运行。
partitioningBy
与groupingBy类似,但是以Boolean为键,有以下两个版本:
//最简单的版本,仅需传入Predicate<? super T>
public static <T>
Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) {
return partitioningBy(predicate, toList());
}
//在基础版本上增加了下游收集器
public static <T, D, A>
Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
Collector<? super T, A, D> downstream) {
}
第47条 Stream要优先用Collection作为返回类型
建议
- 对于public的元素序列返回方法而言,Collection及其子类型通常是最佳的返回类型。
(无法满足需要时再使用自定义集合)
- 不要在内存中保存巨大的序列,将其作为集合返回即可。
(参考给出的幂集合的例子)
foreach遍历Stream
由于Stream并未直接实现Iterable接口,想要使用foreach对Stream进行遍历需要使用适配器进行转换:
// Adapter from Stream<E> to Iterable<E>
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
return stream::iterator;
}
Iterable转换为Stream
// Adapter from Iterable<E> to Stream<E>
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
}
第48条 谨慎使用Stream并行
建议
1.如果Stream的源头来自Stream.iterate,或者使用了limit的中间操作,那么parallel无法提升性能。
2.当以ArrayList、HashMap、HashSet、ConcurrentHashMap、arrays、 int范围、long范围作为Stream来源时,使用并行获得的性能是最好的。
这些数据结构的共性一是:都可以被精确且容易地划分为任意大小的子范围,这使得并行线程中的任务分工变的简单。共性二是:在进行顺序方位时,具有优异的引用局部性;如果在没有引用局部性,那么线程就会出现闲置,需要等待数据从内存转移到处理器的缓存中。
关于引用局部性可参考:从缓存来看局部性提高程序运行效率的原因
3.Stream pipeline的终止操作也影响了并发执行的效率,如果终止操作的工作量占据了所有pipeline的大部分,并且终止操作是固有的顺序(即无法提升到上一级),那么并行的效率就会受到限制。并行的最佳终止操作是做减法(reductions)。
JDK 包含许多终端操作(如 average, sum, min, max,和 count 等通过组合一个流的内容返回一个值,在 java.util.stream.IntStream 中)。 这些操作被称为 reduce操作 。JDK 还包含返回集合而不是单个值的简化操作。许多简化操作执行特定任务, 例如查找值的平均值或将元素分组到各个类别中。这些通常被称为可变的reduce操作,与 reduce 方法处理元素时始终创建新值的方法不同,collect 方法修改或改变现有值。可变的reduce操作不是并行的最好选择,因为合并集合的成本非常高。
4.并行不仅可能降低性能,还可能会导致活性失败,结果出错,以及难以预计的行为(如安全性失败)。
Safety failures may result from parallelizing a pipeline that uses mappers, filters, and other programmer-supplied function objects that fail to adhere to their specifications. The Stream specification places stringent requirements on these function objects. For example, the accumulator and combiner functions passed to Stream’s reduce operation must be associative, non-interfering, and stateless. If you violate these requirements (some of which are discussed in Item 46) but run your pipeline sequentially, it will likely yield correct results; if you paral- lelize it, it will likely fail, perhaps catastrophically.
即便满足了上述条件,即使用了一个可以有效切割的Stream源、一个可并行化或简单的终止操作、互不影响的函数对象,也无法保证能够通过并行进行提速,除非pipeline完成了足够的工作,抵消了并行相关的成本。作为一个非常粗略的估计,流中元素的数量乘以每个元素执行的代码行数应该至少为十万。参见doug lea的文章:When to use parallel streams
5.在适当的情况下,只需向流管道添加一个parallel
方法调用,就可以实现处理器内核数量的近似线性加速。
总之,甚至不要尝试并行化流管道,除非你有充分的理由相信它将保持计算的正确性并提高其速度。不恰当地并行化流的代价可能是程序失败或性能灾难。如果您认为并行性是合理的,那么请确保您的代码在并行运行时保持正确,并在实际情况下进行仔细的性能度量。如果您的代码是正确的,并且这些实验证实了您对性能提高的怀疑,那么并且只有这样才能在生产代码中并行化流。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。