Java8使用lambda实现Java的尾递归

前言

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

什么是尾递归

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

递归实现

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

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

5
  4
    3
      2
        1
------------------->

栈的深度

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

尾递归实现

任何递归的尾递归版本都十分简单,分析上面栈溢出的原因就是在每次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)完成每个栈帧之间的连接,除此之外,我们栈帧还需要定义几个方法来丰富这个尾递归的接口。

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使用lambda实现Java的尾递归,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • Java8新特性lambda表达式有什么用(用法实例)

    我们期待了很久lambda为java带来闭包的概念,但是如果我们不在集合中使用它的话,就损失了很大价值.现有接口迁移成为lambda风格的问题已经通过default methods解决了,在这篇文章将深入解析Java集合里面的批量数据操作(bulk operation),解开lambda最强作用的神秘面纱. 1.关于JSR335 JSR是Java Specification Requests的缩写,意思是Java 规范请求,Java 8 版本的主要改进是 Lambda 项目(JSR 335),其

  • Java8新特性之Lambda表达式浅析

    说到java 8,首先会想到lambda(闭包)以及虚拟扩展方法(default method),这个特性早已经被各大技术网站炒得沸沸扬扬了,也是我们java 8系列开篇要讲的第一特性(JEP126 http://openjdk.java.net/jeps/126),jdk8的一些库已经应用了lambda表达式重新设计了,理解他对学习java 8新特性有着重要的意义. 一.函数式接口 函数式接口(functional interface 也叫功能性接口,其实是同一个东西).简单来说,函数式接口是

  • Java8中的 Lambda表达式教程

     1. 什么是λ表达式 λ表达式本质上是一个匿名方法.让我们来看下面这个例子: public int add(int x, int y) { return x + y; } 转成λ表达式后是这个样子: (int x, int y) -> x + y; 参数类型也可以省略,Java编译器会根据上下文推断出来: (x, y) -> x + y; //返回两数之和 或者 (x, y) -> { return x + y; } //显式指明返回值 可见λ表达式有三部分组成:参数列表,箭头(-&g

  • Java8 新特性Lambda表达式实例详解

    Java8 新特性Lambda表达式实例详解 在介绍Lambda表达式之前,我们先来看只有单个方法的Interface(通常我们称之为回调接口): public interface OnClickListener { void onClick(View v); } 我们是这样使用它的: button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { v.setText("

  • Java8 Lambda表达式详解及实例

    第一个Lambda表达式 在Lambda出现之前,如果我们需要写一个多线程可能需要下面这种方式: Runnable runnable = new Runnable() { @Override public void run() { System.out.println("Hello runnable"); } }; ... thread.start(); 上面的例子如果改成使用Lambda就会简单许多: Runnable noArgs = ()->System.out.print

  • Java8中lambda表达式的应用及一些泛型相关知识

    语法部分就不写了,我们直接抛出一个实际问题,看看java8的这些新特性究竟能给我们带来哪些便利 顺带用到一些泛型编程,一切都是为了简化代码 场景: 一个数据类,用于记录职工信息 public class Employee { public String name; public int age; public char sex; public String time; public int salary; } 我们有一列此类数据 List<Employee> data = Arrays.asL

  • Java8使用lambda实现Java的尾递归

    前言 本篇介绍的不是什么新知识,而是对前面讲解的一些知识的综合运用.众所周知,递归是解决复杂问题的一个很有效的方式,也是函数式语言的核心,在一些函数式语言中,是没有迭代与while这种概念的,因为此类的循环通通可以用递归来实现,这类语言的编译器都对递归的尾递归形式进行了优化,而Java的编译器并没有这样的优化,本篇就要完成这样一个对于尾递归的优化. 什么是尾递归 本篇将使用递归中最简单的阶乘计算来作为例子 递归实现 /** * 阶乘计算 -- 递归解决 * * @param number 当前阶

  • Java8之lambda表达式基本语法

    lambda表达式,即带有参数的表达式,为更清晰地理解lambda表达式,先看如下例子: (1) class Student{ private String name; private Double score; public Student(String name, Double score) { this.name = name; this.score = score; } public String getName() { return name; } public Double getS

  • java8之lambda表达式用法总结

    java8之lambda表达式 目的:行为参数化 Lambda表达式是简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表.函数主体.返回类型,可能还有一个可以抛出的异常列表. Lambda的基本语法是(parameters) -> expression 或 (parameters) -> { statements; }.其中, (parameters) -> expression 的表达式中隐含了return,如 () -> 42: (parameters) ->

  • Java8的Lambda表达式你真的会吗

    理解Lambda Lambda表达式可以是一段可以传递的代码,它的核心思想是将面向对象中的传递数据变成传递行为,也就是行为参数化,将不同的行为作为参数传入方法. 随着函数式编程思想的引进,Lambda表达式让可以用更加简洁流畅的代码来代替之前冗余的Java代码. 口说无凭,直接上个例子吧.在Java8之前,关于线程代码是这样的: class Task implements Runnable{ @Override public void run() { System.out.println("Ja

  • 一文带你掌握Java8中Lambda表达式 函数式接口及方法构造器数组的引用

    目录 函数式接口概述 函数式接口示例 1.Runnable接口 2.自定义函数式接口 3.作为参数传递 Lambda 表达式 内置函数式接口 Lambda简述 Lambda语法 方法引用 构造器引用 数组引用 函数式接口概述 只包含一个抽象方法的接口,称为函数式接口. 可以通过 Lambda 表达式来创建该接口的对象. 可以在一个接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口.同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口.

  • Java8的Lambda和排序

    目录 对数组和集合进行排序是Java 8 lambda令人惊奇的一个应用,我们可以实现一个Comparators来实现各种排序. 看下面案例: static class Person { final String firstName; final String lastName; Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } @Override

  • Java8中Lambda表达式的理解与应用

    目录 简介 正文 1. lambda的语法 2. 为啥引入lambda 3. 什么是函数式接口 4. 什么是行为参数化 5. 手写一个函数式接口 6. 常用的函数式接口 7. 什么是方法引用 8. 什么是构造引用 9. lambda表达式中引入外部变量的限制 10. lambda的组合操作 总结 简介 Lambda表达式是一个可传递的代码块,可以在以后执行一次或多次: 下面贴个对比代码: // Java8之前:旧的写法 Runnable runnable = new Runnable() { @

  • Java8中 LocalDate和java.sql.Date的相互转换操作

    一.简述 首先,Java 8引入了java.time.LocalDate来表示一个没有时间的日期. 其次,使用Java 8版本,还需要更新java.sql.Date,以便为LocalDate提供支持,包括toLocalDate和valueOf(LocalDate)等方法. 二.java.time.LocalDate转换为java.sql.Date java.sql.Date.valueOf( localDate ) package insping; public class Test { pub

  • Java8之lambda最佳实践_动力节点Java学院整理

    在8 里面Lambda是最火的主题,不仅仅是因为语法的改变,更重要的是带来了函数式编程的思想,我觉得优秀的程序员,有必要学习一下函数式编程的思想以开阔思路.所以这篇文章聊聊Lambda的应用场景,性能,也会提及下不好的一面. Java为何需要Lambda 1996年1月,Java 1.0发布了,此后计算机编程领域发生了翻天覆地的变化.商业发展需要更复杂的应用,大多数程序都跑在更强大的装备多核CPU的机器上.带有高效运行期编译器的Java虚拟机(JVM)的出现,使得程序员将精力更多放在编写干净.易

随机推荐