主要是非常简洁,也为函数式编程提供了一定的支持。在大部分情况下,Lambda代替匿名类还是比较舒服的,正因为如此,需要了解lambda的边界。
演示一种比特定于常量的方法更简洁清晰的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);
}
}
Java提供了比lambda更加简洁的方法引用实现。
// 如果不存在key,则put(key, 1),否则put(key, get(key) + 1)
map.merge(key, 1, (count, incr) -> count + incr);
// count和incr没有意义,使用方法引用简化
map.merge(1, 1, Integer::sum);
在大部分情况下,方法引用都比lambda要简洁许多,但是部分场景lambda更加清晰,例如引用本身的方法。
// 方法引用
service.execute(GoshThisClassNameIsHumongous::action);
// lambda
service.execute(() -> action());
因而方法引用能更加简洁的时候,就优先方法引用。
方法引用并不一定要引用静态方法,一共有四种不同的使用场景。
类型 | 方法引用 | lambda |
---|---|---|
静态方法 | Integer::parseInt | str -> Integer.parseInt(str) |
实例方法 | Instant.now()::isAfter | Instant then = Instant.now(); |
t -> then.isAfter(t) | ||
String::toLowerCase | str -> str.toLowerCase() | |
初始化方法 | TreeMap |
() -> new TreeMap |
Array Constructor | ||
int[]::new | len -> new int[len] |
Lambda表达式的设计,让使用者并不需要记住用的到底是哪种函数接口,另一方面,函数接口有时候会仅仅靠名字来坐区分——它们的功能一模一样,接口名不同或函数名不同。这种命名的区分对于API的设计是有意义的。不过也正是由于这种相似性,大多数时候,我们不需要自己编写函数接口,书中建议:
坚持使用标准的函数接口。
同时也有一些需要注意的地方:
@FuntionalInterface
的作用是标记函数接口。但建议坚持使用,因为这种标注是有意义的:1. 告诉使用者,这是函数接口。2. 避免后续被添加额外的方法或抽象方法。Stream的API用着非常舒服,会让刚刚接触的人巴不得把所有的迭代转换为stream,然而一些时候会牺牲可读性,为了确保这一点,必须了解一些使用stream的最佳实践。
不要滥用Stream而影响程序的可读性,尤其是复杂的迭代,应该混合使用迭代和stream。
这一条主要在介绍Collectors的API,其他可以和上一条合并。
Stream的使用应该尽量遵守Stream范式,最重要的是要保证每一级操作都尽可能为纯函数——不依赖任何状态,也不改变任何状态,函数的输出只和输入有关。
有时候集合类是可迭代的(实现了Iterable),但可能并没有提供生成stream的方法,而Stream也没有原生支持迭代(例如对Stream使用forEach循环),这个其实有点奇怪,因为二者从功能上看是类似的。
因此书中建议返回Collection对象,因为Collection实现了Iterable,同时也提供了stream生成方法,所以它同时支持迭代和流操作。从这个意义上看,Collection类似一个适配器类。】
当然你也可以自己编写适配方法,因为本质上stream的底层就是使用了迭代,它们之间可以通过适配方法转换,但这样比较麻烦。
// Adapter from Stream to Iterable
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
return stream::iterator;
}
// Adapter from Iterable to Stream
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
}
并不是所有时候都能返回一个完整集合的,极端情况例如幂集(子集数),或者素数集,或者更多无限的集合,我们并没有办法生成一个列表,直接返回给用户。考虑到效率问题,大于等于O(n * n)开销的列表,通常就无法被接受。
这个时候,Stream就可以发挥作用的,因为它是函数式编程——惰性的,只有在迭代的时候才会按照设定的规则去运行。当然通过迭代器也可以手动实现,stream底层就是通过迭代器+函数接口实现的,但显然函数式编程要简单强大得多。
// 列表子列表生成,迭代实现,集合空间开销O(n * n)
for (int start = 0; start < src.size(); start++)
for (int end = start + 1; end <= src.size(); end++)
System.out.println(src.subList(start, end));
// stream实现,尽管看起来比较难懂,但它是惰性的,并不需要直接返回集合
// Returns a stream of all the sublists of its input list
public static <E> Stream<List<E>> of(List<E> list) {
return IntStream.range(0, list.size())
.mapToObj(start ->
IntStream.rangeClosed(start + 1, list.size())
.mapToObj(end -> list.subList(start, end)))
.flatMap(x -> x);
}
有时候可以通过精巧的设计自定义的Collection来满足提高性能,例如上面提到的子集列表,可以通过子集数和Index的位向量的映射来实现。
// Returns the power set of an input set as custom collection
public class PowerSet {
public static final <E> Collection<Set<E>> of(Set<E> s) {
List<E> src = new ArrayList<>(s);
if (src.size() > 30)
throw new IllegalArgumentException("Set too big " + s);
return new AbstractList<Set<E>>() {
@Override public int size() {
return 1 << src.size(); // 2 to the power srcSize
}
@Override public boolean contains(Object o) {
return o instanceof Set && src.containsAll((Set)o);
}
@Override public Set<E> get(int index) {
Set<E> result = new HashSet<>();
for (int i = 0; index != 0; i++, index >>= 1)
if ((index & 1) == 1)
result.add(src.get(i));
return result;
}
};
}
}
虽然S0tream的一大强大的特性就是可以通过parallel()
开启并行操作,提高性能。但我们很自然地可以理解,并行或并发安全并不是一件容易的事,只有部分任务才适合并发/并行执行,类似可以参考MapReduce。
所以建议使用Stream并行必须经过测试,确保性能提升而又没有安全问题。