Java8函数之旅 (六) -- 使用lambda实现Java的尾递归

前言

本篇介绍的不是什么新知识,而是对前面讲解的一些知识的综合运用。众所周知,递归是解决复杂问题的一个颇有效的方式,也是函数式语言的核心,在一些函数式语言中,是没有迭代与while这种概念的,由于此类的循环统统能够用递归来实现,这类语言的编译器都对递归的尾递归形式进行了优化,而Java的编译器并无这样的优化,本篇就要完成这样一个对于尾递归的优化。html

什么是尾递归

本篇将使用递归中最简单的阶乘计算来做为例子java

递归实现

/**
     * 阶乘计算 -- 递归解决
     *
     * @param number 当前阶乘须要计算的数值
     * @return number!
     */
    public static int factorialRecursion(final int number) {
        if (number == 1) return number;
        else return number * factorialRecursion(number - 1);
    }

这种方法计算阶乘比较大的数很容易就栈溢出了,缘由是每次调用下一轮递归的时候在栈中都须要保存以前的变量,因此整个栈结构相似是这样的app

5
  4
    3
      2
        1
-------------------> 
      栈的深度

在没有递归到底以前,那些中间变量会一直保存着,所以每一次递归都须要开辟一个新的栈空间ide

尾递归实现

任何递归的尾递归版本都十分简单,分析上面栈溢出的缘由就是在每次return的时候都会附带一个变量,所以只须要在return的时候不附带这个变量便可。提及来简单,该怎么作呢?其实也很容易,咱们使用一个参数来保存上一轮递归的结果,这样就能够了,所以尾递归的阶乘实现应该是这样的代码。函数

/**
     * 阶乘计算 -- 尾递归解决
     *
     * @param factorial 上一轮递归保存的值
     * @param number    当前阶乘须要计算的数值
     * @return number!
     */
    public static int factorialTailRecursion(final int factorial, final int number) {
        if (number == 1) return factorial;
        else return factorialTailRecursion(factorial * number, number - 1);
    }

使用一个factorial变量保存上一轮阶乘计算出的数值,这样return的时候就无需保存变量,整个的计算过程是
(5*4)20 -> (20*3) 60 -> (60*2) 120 -> return 120工具

这样子经过每轮递归结束后刷新当前的栈空间,复用了栈,就克服了递归的栈溢出问题,像这样的return后面不附带任何变量的递归写法,也就是递归发生在函数最尾部,咱们称之为'尾递归'。测试

使用lambda实现编译器的优化

很显然,若是事情这么简单的话,这篇文章也就结束了,和lambda也没啥关系 :) 然而当你调用上文的尾递归写法以后,发现并无什么做用,该栈溢出的仍是会栈溢出,其实缘由我在开头就已经说了,尾递归这样的写法自己并不会有什么用,依赖的是编译器对尾递归写法的优化,在不少语言中编译器都对尾递归有优化,然而这些语言中并不包括java,所以在这里咱们使用lambda的懒加载(惰性求值)机制来延迟递归的调用,从而实现栈帧的复用。优化

设计尾递归的接口

所以咱们须要设计一个这样的函数接口来代替递归中的栈帧,经过apply这个函数方法(取名叫apply是由于该方法的参数是一个栈帧,返回值也是一个栈帧,类比function接口的apply)完成每一个栈帧之间的链接,除此以外,咱们栈帧还须要定义几个方法来丰富这个尾递归的接口。this

  • apply(链接栈帧,惰性求值)
  • 判断递归是否结束
  • 获得递归最后的结果
  • 执行递归(及早求值)

根据上面的几条定义,设计出以下的尾递归接口设计

/**
 * 尾递归函数接口
 * @author : martrix
 */
@FunctionalInterface
public interface TailRecursion<T> {
    /**
     * 用于递归栈帧之间的链接,惰性求值
     * @return 下一个递归栈帧
     */
    TailRecursion<T> apply();

    /**
     * 判断当前递归是否结束
     * @return 默认为false,由于正常的递归过程当中都还未结束
     */
    default boolean isFinished(){
        return false;
    }

    /**
     * 得到递归结果,只有在递归结束才能调用,这里默认给出异常,经过工具类的重写来得到值
     * @return 递归最终结果
     */
    default T getResult()  {
        throw new Error("递归尚未结束,调用得到结果异常!");
    }

    /**
     * 及早求值,执行者一系列的递归,由于栈帧只有一个,因此使用findFirst得到最终的栈帧,接着调用getResult方法得到最终递归值
     * @return 及早求值,得到最终递归结果
     */
    default T invoke() {
        return Stream.iterate(this, TailRecursion::apply)
                .filter(TailRecursion::isFinished)
                .findFirst()
                .get()
                .getResult();
    }
}

设计对外统一的尾递归包装类

为了达到能够复用的效果,这里设计一个尾递归的包装类,目的是用于对外统一方法,使得须要尾递归的调用一样的方法便可完成尾递归,不须要考虑内部实现细节,由于全部的递归方法无非只有2类类型的元素组成,一个是怎样调用下次递归,另一个是递归的终止条件,所以包装方法设计为如下两个

  • 调用下次递归
  • 结束本轮递归
    代码以下
/**
 * 使用尾递归的类,目的是对外统一方法
 *
 * @author : Matrix
 */
public class TailInvoke {
    /**
     * 统一结构的方法,得到当前递归的下一个递归
     *
     * @param nextFrame 下一个递归
     * @param <T>       T
     * @return 下一个递归
     */
    public static <T> TailRecursion<T> call(final TailRecursion<T> nextFrame) {
        return nextFrame;
    }

    /**
     * 结束当前递归,重写对应的默认方法的值,完成状态改成true,设置最终返回结果,设置非法递归调用
     *
     * @param value 最终递归值
     * @param <T>   T
     * @return 一个isFinished状态true的尾递归, 外部经过调用接口的invoke方法及早求值, 启动递归求值。
     */
    public static <T> TailRecursion<T> done(T value) {
        return new TailRecursion<T>() {
            @Override
            public TailRecursion<T> apply() {
                throw new Error("递归已经结束,非法调用apply方法");
            }

            @Override
            public boolean isFinished() {
                return true;
            }

            @Override
            public T getResult() {
                return value;
            }
        };
    }
}

完成阶乘的尾递归函数

经过使用上面的尾递归接口与包装类,只须要调用包装了call与done就能够很轻易的写出尾递归函数,代码以下

/**
     * 阶乘计算 -- 使用尾递归接口完成
     * @param factorial 当前递归栈的结果值
     * @param number 下一个递归须要计算的值
     * @return 尾递归接口,调用invoke启动及早求值得到结果
     */
    public static TailRecursion<Integer> factorialTailRecursion(final int factorial, final int number) {
        if (number == 1)
            return TailInvoke.done(factorial);
        else
            return TailInvoke.call(() -> factorialTailRecursion(factorial + number, number - 1));
    }

经过观察发现,和原先预想的尾递归方法几乎如出一辙,只是使用包装类的call与done方法来表示递归的调用与结束
预想的尾递归

/**
     * 阶乘计算 -- 尾递归解决
     *
     * @param factorial 上一轮递归保存的值
     * @param number    当前阶乘须要计算的数值
     * @return number!
     */
    public static int factorialTailRecursion(final int factorial, final int number) {
        if (number == 1) return factorial;
        else return factorialTailRecursion(factorial * number, number - 1);
    }

测试尾递归函数

这里做一个说明,由于阶乘的计算若是要计算到栈溢出通常状况下Java的数据类型须要使用BigInteger来包装,为了简化代码,这里的测试仅仅是是测试栈会不会溢出的问题,所以咱们将操做符的*改为+这样修改的结果仅仅是结果变小了,可是栈的深度却没有改变。测试代码以下
首先测试 深度为10W的普通递归

测试代码

@Test
    public void testRec() {
        System.out.println(factorialRecursion(100_000));
    }

理所固然的栈溢出了

java.lang.StackOverflowError
    at test.Factorial.factorialRecursion(Factorial.java:20)
    at test.Factorial.factorialRecursion(Factorial.java:20)
    at test.Factorial.factorialRecursion(Factorial.java:20)
    at test.Factorial.factorialRecursion(Factorial.java:20)
    at test.Factorial.factorialRecursion(Factorial.java:20)
    
Process finished with exit code -1

这里咱们测试1000W栈帧的尾递归
尾递归代码

public static TailRecursion<Long> factorialTailRecursion(final long factorial, final long number) {
        if (number == 1)
            return TailInvoke.done(factorial);
        else
            return TailInvoke.call(() -> factorialTailRecursion(factorial + number, number - 1));
    }

测试代码

@Test
    public void testTailRec() {
        System.out.println(factorialTailRecursion(1,10_000_000).invoke());
    }

发现结果运转良好

50000005000000

Process finished with exit code 0

因为阶乘的计算通常初始值都为1,因此再进一步包装一下,将初始值设置为1

public static long factorial(final long number) {
        return factorialTailRecursion(1, number).invoke();
    }

最终调用代码以下,彻底屏蔽了尾递归的实现细节

@Test
    public void testTailRec() {
        System.out.println(factorial(10)); //结果为 3628800
    }

总结

本文讲解了利用lambda懒加载的特性完成了递归中栈帧的复用,实现了函数式语言编译器的'尾递归'优化,虽然上面的例子很简单,可是设计的接口和包装类都是通用的,能够说任何须要使用尾递归的均可以使用上面的代码来实现尾递归的优化,这也算是为编译器帮了点忙吧。

上一篇:开始Java8之旅(五) -- Java8中的排序
下一篇:开始Java8之旅(七) -- 函数式备忘录模式优化递归

相关文章
相关标签/搜索