Effective Java Third Edition学习 - Lambdas and Streams

Prefer lambdas to anonymous classes

匿名类太麻烦,让Java不适合做函数式编程。
Java8里,只有一个方法的接口,被特殊对待。这些接口现在叫函数式接口,允许你使用 lambda表达式生成这些类的实例。Lambdas类似于匿名类,只是更简洁。
忽略所有 lambda参数的类型吧,除非他们的存在使你的程序更清晰。如果编译器生成一个错误,告诉你不能推断lambda参数的类型,只能指定它。有时候,你不得不抛弃返回值或者整个 lambda表达式,但是,这太罕见了。
增加了 lambdas,可以无感知地使用函数对象。
不像方法和类,lambda缺乏名字和文档;如果计算不是不言自明的,或者超过了几行,不要使用lambda。最理想的是只有一行,最多三行。如果违反该规则,代码的可读性就会变差。如果lambda太长,或者不好读,要么想办法简化,要么别用。
同样,你可能觉得匿名类过时了。这接近真相,但是,有时候你能用匿名类而不能用lambda。lambda被限制成函数式接口。如果你想生成一个抽象类的实例,你可以用匿名类,不能用lambda。同样,你能用匿名类生成有多个抽象方法的接口的实例。最后,lambda不能获取它自己的引用。在lambda里,关键字引用的是封闭的实例,它是你想要的那个类型。在匿名类里,关键字引用的是该匿名类的实例。如果你需要访问内部的函数对象,你必须用匿名类。
你不要序列化一个lambda(或者匿名类实例)。

Prefer method references to lambdas

lambdas和匿名类相比,最主要的优点是更简洁。Java提供了一个比lambda还要简洁的生成函数对象的办法:方法引用。
你能用方法引用,就一定能用lambda(有一个晦涩的异常,见JLS, 9.9-2)。方法引用一般更短更清晰。如果一个lambda太长或者复杂,你可以从lambda里抽取代码,放进一个新方法,然后,把lambda替换成新写的方法。你能给方法起个好名字,和好文档。

Favor the use of standard functional interfaces

Java现在有了lambda,写API的最佳做法有了比较大的变化。例如, Template Method模式[Gamma95],在子类里覆盖一个原始方法,没什么吸引力了。更好的办法是提供一个静态工厂或者构造器,接收一个函数对象。更一般地,你将写更多的把函数对象作为参数的构造器和方法。要关心选择正确的函数式参数类型。
java.util.function提供了很多标准函数式接口给你使用。如果一个标准函数式接口能做这工作,你应该使用它。这样你的API容易学习。
如果标准的不能满足你的需求,只好写自己的函数式接口。例如,你需要使用三个参数做断言,或者抛检查式异常的接口。甚至跟标准的结构一致时,也应该写自己的。
最后再注意一点,不要提供有多个重载的方法,不同的接口有相同的参数会增加歧义。这是个现实的问题。

Use streams judiciously

你应该不要用流处理char值。
当你开始使用流,你可能想用流替换所有的循环,但是,不要这样。这样将不好读和维护。混合使用流和迭代,更适合完成复杂任务。
用代码块,你能读或者修改适用范围内的局部变量;用lambda,你只能读final的或者有效的final变量[JLS 4.12.4],你不能修改任何局部变量。
使用代码块,你能从封闭的方法返回,break或者continue一个封闭的循环,能抛方法声明的受检异常;使用lambda,你做不到这些。
使用流,想在一个管道的多个步骤都访问相应的元素,是很困难的:一旦做了一个值的映射,原始值就丢了。一个解决办法是把原始值和新值映射成一对对象,这个 解决办法不让人满意,特别是管道的多个步骤都需要对象对的时候。

Prefer side-effect-free functions in streams

流范式的最重要部分是把你的计算构造成一系列变换,每一步的结果都尽可能成为前一步结果的纯函数。纯函数只依赖输入就可以返回,跟状态无关或者不更新任何状态。为了实现它,你传进流运算的任何函数对象,中间的和结束的,都应该没有副作用。

Prefer Collection to Stream as a return type

使用适配器,你能在任何流上,使用 for-each语句做迭代。
如果你写的一个公共API,返回一个序列,你就同时应该提供给写流管道和 for-each语句的用户,除非你有好的理由相信大多数用户都使用相同的机制。

Use caution when making streams parallel

不要不分青红皂白地使用并行流管道。性能可能很差。
另一个重要的事实是,所有的这些数据结构的共同之处是,当顺序处理时,他们提供了良好的局部引用:在内存中,顺序的元素引用保存在一起。内存中,这些引用指向的对象可能不靠在一起,它减少了局部引用。对于并行块运算来说,局部引用非常重要:没有它,线程很多时候处于空闲状态,等待数据从内存传输到处理器缓存。拥有最好的局部引用的数据结构是原始数组,因为在内存里,它的数据是连续存储的。
如果你写自己的 Stream、 Iterable或者Collection实现,想并行执行,你必须覆盖 spliterator方法,广泛测试性能。写高质量的 spliterators是很困难的,超出了本书的范围。
不只是并行的流能导致性能恶化,或者活性故障;它还能导致不正确的结果和不可预测的行为(安全故障)。使用并行流的mappers、filters 和程序提供的其他函数对象,没有遵守他们的规则,可能导致安全故障。
通常,程序里的所有并行流管道运行在 fork-join池内。一个行为不端的管道会危害系统内不相干部分的性能。
如果你用并行的流计算随机数,SplittableRandom实例比 ThreadLocalRandom(或者过时的Random)好。 SplittableRandom就是为此设计的,能获得线性提升。 ThreadLocalRandom是给单线程使用的,能适应并行流数据源的功能,但是没 SplittableRandom快。Random的每个运算都是同步的,导致过度的并行争用。

猜你喜欢

转载自blog.csdn.net/weixin_43364172/article/details/83993423