Lambda和Stream

42. Lambda优先于匿名类

主要是非常简洁,也为函数式编程提供了一定的支持。在大部分情况下,Lambda代替匿名类还是比较舒服的,正因为如此,需要了解lambda的边界。

  • 一行最为理想,三行以上就不要用lambda了
  • lambda只支持函数式接口,匿名类可以支持抽象类的匿名实例的创建
  • lambda不能获得自身的引用,this指向外围类,匿名类可以
  • 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);
    }
}

43. 方法引用优先于lambda

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 () -> new TreeMap
Array Constructor
int[]::new len -> new int[len]

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

Lambda表达式的设计,让使用者并不需要记住用的到底是哪种函数接口,另一方面,函数接口有时候会仅仅靠名字来坐区分——它们的功能一模一样,接口名不同或函数名不同。这种命名的区分对于API的设计是有意义的。不过也正是由于这种相似性,大多数时候,我们不需要自己编写函数接口,书中建议:

坚持使用标准的函数接口。

同时也有一些需要注意的地方:

  • 标准函数接口提供了基本类型的定制版本,主要是为了避开装箱拆箱带来的额外性能开销,这在批量操作的时候提升是比较大的。顺便一提,猜测正是考虑效率,编译器会把lambda编译成方法,而不是独立的类。
  • @FuntionalInterface的作用是标记函数接口。但建议坚持使用,因为这种标注是有意义的:1. 告诉使用者,这是函数接口。2. 避免后续被添加额外的方法或抽象方法。
  • 自定义函数接口的理由有如下两个:会收益于描述性的命名;会受益于默认的方法。

45. 明智地使用Stream

Stream的API用着非常舒服,会让刚刚接触的人巴不得把所有的迭代转换为stream,然而一些时候会牺牲可读性,为了确保这一点,必须了解一些使用stream的最佳实践。

  • Lambda没有显式类型,因而仔细命名Lambda的参数,对于stream程序的可读性至关重要的。
  • 不要在Stream中保留太多实现细节,适当地使用helper方法进行封装,这对于stream来说,远比对迭代来得重要。
  • 避免用Stream处理char

不要滥用Stream而影响程序的可读性,尤其是复杂的迭代,应该混合使用迭代和stream。

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

这一条主要在介绍Collectors的API,其他可以和上一条合并。
Stream的使用应该尽量遵守Stream范式,最重要的是要保证每一级操作都尽可能为纯函数——不依赖任何状态,也不改变任何状态,函数的输出只和输入有关。

  • forEach就是一个容易改变状态的函数,我们经常想用forEach去替换迭代,但是这种替换实际上只是伪装成stream的迭代代码,它享受不到Stream带来的好处,只会降低可读性。

47. Stream相关方法优先用Collection作为返回类型

优先Collection

有时候集合类是可迭代的(实现了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;
            }
        };
    }
}

48. 明智地使用Stream并行

虽然S0tream的一大强大的特性就是可以通过parallel()开启并行操作,提高性能。但我们很自然地可以理解,并行或并发安全并不是一件容易的事,只有部分任务才适合并发/并行执行,类似可以参考MapReduce。
所以建议使用Stream并行必须经过测试,确保性能提升而又没有安全问题。

你可能感兴趣的:(Java后台,Java高级,books,java,开发语言,后端)