Java 设计模式最佳实践:1~5(3)https://developer.aliyun.com/article/1426753
尾部调用优化
尾部调用优化(TCO)是一些编译器在不使用栈空间的情况下调用函数的技术。Scala 通过用@tailrec
注解递归代码来利用它。这基本上告诉编译器使用一个特殊的循环,称为 trampoline,它反复运行函数。函数调用可以处于一种或多种要调用的状态。在完成时,它返回结果(头部),在更多的情况下,它返回当前循环而不返回头部(尾部)。这个模式已经被 cyclops-react 提供给我们了。
意图
其目的是在不破坏栈的情况下启用递归调用。它只用于大量的递归调用,对于少数调用,它可能会降低性能。
示例
cyclops-react 的维护者 John McClean 演示了 TCO 在 Fibonacci 序列中计算数字的用法。代码简洁易懂,基本上是从初始状态 0 和 1 开始累加斐波那契数,f(0) = 0
、f(1) = 1
,应用f(n) = f(n-1) + f(n-2)
函数:
importstatic cyclops.control.Trampoline.done; importstatic cyclops.control.Trampoline.more; import cyclops.control.Trampoline; publicclass Main { publicvoid fib() { for(int i=0;i<100_000;i++) System.out.println(fibonacci(i, 0l, 1l).get()); } public Trampoline<Long> fibonacci(Integer count, Long a, Long b) { return count==0 ? done(a) : more(()->fibonacci (count - 1, b, a + b)); } publicstaticvoid main(String[] args) { new Main().fib(); } }
回忆录
多次调用前面的 Fibonacci 实现将导致 CPU 周期的浪费,因为有些步骤是相同的,并且我们可以保证,对于相同的输入,我们总是得到相同的输出(纯函数)。为了加速调用,我们可以缓存输出,对于给定的输入,只返回缓存结果,而不是实际计算结果。
意图
其目的是缓存给定输入的函数结果,并使用它加速对给定相同输入的相同函数的进一步调用。它应该只用于纯函数,因为它们提供了引用透明性。
示例
在下面的示例中,我们将重用 Fibonacci 代码并添加 Guava 缓存。缓存将保存 Fibonacci 的返回值,而键是输入数字。缓存配置为在大小和时间上限制内存占用:
importstatic cyclops.control.Trampoline.done; importstatic cyclops.control.Trampoline.more; import java.math.BigInteger; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import cyclops.async.LazyReact; import cyclops.control.Trampoline; publicclass Main { public BigInteger fib(BigInteger n) { return fibonacci(n, BigInteger.ZERO, BigInteger.ONE).get(); } public Trampoline<BigInteger> fibonacci(BigInteger count, BigInteger a, BigInteger b) { return count.equals(BigInteger.ZERO) ? done(a) : more(()->fibonacci (count.subtract(BigInteger.ONE), b, a.add(b))); } publicvoid memoization(List<Integer> array) { Cache<BigInteger, BigInteger> cache = CacheBuilder.newBuilder() .maximumSize(1_000_000) .expireAfterWrite(10, TimeUnit.MINUTES) .build(); LazyReact react = new LazyReact().autoMemoizeOn((key,fn)-> cache.get((BigInteger)key,()-> (BigInteger)fn. apply((BigInteger)key))); Listresult = react.from(array) .map(i->fibonacci(BigInteger.valueOf(i), BigInteger.ZERO, BigInteger.ONE)) .toList(); } publicstaticvoid main(String[] args) { Main main = new Main(); List<Integer> array = Arrays.asList(500_000, 499_999); long start = System.currentTimeMillis(); array.stream().map(BigInteger::valueOf).forEach(x -> main.fib(x)); System.out.println("Regular version took " + (System.currentTimeMillis() - start) + " ms"); start = System.currentTimeMillis(); main.memoization(array); System.out.println("Memoized version took " + (System.currentTimeMillis() - start) + " ms"); } }
输出如下:
Regular version took 19022 ms Memoized version took 394 ms
环绕执行方法
在度量每个版本的代码的性能时,前面的代码似乎都在重复。这可以通过环绕执行方法模式解决,方法是将执行的业务代码包装到 Lambda 表达式中。这种模式的一个很好的例子是单元测试前后的设置/拆卸函数。这类似于前面描述的模板方法和借贷模式。
意图
其目的是让用户可以在特定业务方法之前和之后执行某些特定的操作。
示例
上一个示例中提到的代码包含重复的代码(代码气味)。我们将应用环绕执行模式来简化代码并使其更易于阅读。可能的重构可以使用 Lambda,如我们所见:
publicstaticvoid measurePerformance(Runnable runnable) { long start = System.currentTimeMillis(); runnable.run(); System.out.println("It took " + (System.currentTimeMillis() - start) + " ms"); } publicstaticvoid main(String[] args) { Main main = new Main(); List<Integer> array = Arrays.asList(500_000, 499_999); measurePerformance(() -> array.stream().map(BigInteger::valueOf) .forEach(x -> main.fib(x))); measurePerformance(() -> main.memoization(array)); }
总结
在本章中,我们了解了函数式编程的含义、最新 Java 版本提供的特性,以及它们是如何改变一些现有的 GOF 模式的。我们还使用了一些函数式编程设计模式。
在下一章中,我们将深入到反应式世界,学习如何使用 RxJava 创建响应式应用。