Java中的堆栈安全递归

stack_safe_recursion_in_Java_ebook 在本文中,摘自《 Java中的函数式编程 》一书,我解释了如何使用递归,同时避免了StackOverflow异常的风险。

Corecursion正在使用第一步的输出作为下一步的输入来构成计算步骤。 递归是相同的操作,但是从最后一步开始。 在这种情况下,我们必须延迟评估,直到遇到基本条件(与corecursion的第一步相对应)为止。

假设我们的编程语言中只有两条指令:递增(向值加1)和递减(从值中减去1)。 让我们通过编写这些指令来实现加法。

Corecursive和递归加法示例

为了将两个数字x和y相加,我们可以执行以下操作:

  • 如果y == 0 ,则返回x
  • 否则,递增x ,递减y ,然后重新开始。

这可以用Java编写为:

static int add(int x, int y) {while(y > 0) {x = ++x;y = --y;}return x;
}

或更简单:

static int add(int x, int y) {while(y-- > 0) {x = ++x;}return x;
}

注意,直接使用参数xy没问题,因为在Java中,所有参数都是按值传递的。 另请注意,我们已使用后减量来简化编码。 但是,我们可以通过稍微改变条件来使用预减量,从而将形式从y迭代为1到将y‑1迭代为0

static int add(int x, int y) {while(--y >= 0) {x = ++x;}return x;
}

递归版本比较棘手,但仍然非常简单:

static int addRec(int x, int y) {return y == 0? x: addRec(++x, --y);
}

两种方法似乎都可行,但是如果我们尝试使用大量的递归版本,可能会感到惊讶。 虽然

addRec(10000, 3);

切换参数,产生预期结果10003,如下所示:

addRec(3, 10000);

产生一个StackOverflowException

如何用Java实现递归?

要了解正在发生的事情,我们必须查看Java如何处理方法调用。 调用方法时,Java会挂起当前正在执行的操作,并将环境压入堆栈以为调用的方法执行留出空间。 当此方法返回时,Java弹出堆栈以恢复环境并恢复程序执行。 如果我们依次调用一个方法,则堆栈将始终保存这些方法调用环境中的至少一个。

但是方法不仅是通过一个接一个地调用它们而构成的。 方法调用方法。 如果method1作为其实现的一部分调用method2 ,则Java会再次挂起method1执行,将当前环境压入stack ,然后开始执行method2 。 当method2返回时,Java从堆栈中弹出最后推送的环境并恢复执行(在本例中为method1 )。 当method1完毕后,Java从栈中弹出一次,并恢复它在调用此方法之前做的事情。

当然,方法调用可能嵌套得很深。 方法嵌套深度是否有限制? 是。 限制是堆栈的大小。 在当前情况下,该限制约为几千个级别,尽管可以通过配置堆栈大小来增加此限制。 但是,所有线程都使用相同的堆栈大小,因此增加单个计算的堆栈大小通常会浪费空间。 默认堆栈大小在320k和1024k之间变化,具体取决于Java版本和所使用的系统。 对于具有最小堆栈使用率的64位Java 8程序,嵌套方法调用的最大数量约为7000。通常,除了非常特殊的情况外,我们不需要更多的嵌套方法调用。 一种这样的情况是递归方法调用。

消除尾调用(TCE)似乎有必要将环境推送到堆栈上,以便允许在被调用方法返回后恢复计算。 但不总是。 如果对方法的调用是调用方法中的最后一件事,则返回时没有任何恢复操作,因此可以直接与当前方法的调用者而不是当前方法本身一起恢复。 在最后一个位置发生的方法调用(即返回之前的最后一件事)称为tail call 。 避免在尾部调用之后将环境压入堆栈以恢复方法处理是一种称为尾部消除(TCE)的优化技术。 不幸的是,Java没有实现TCE。

消除尾声有时被称为尾声优化(TCO)。 TCE通常是一种优化,我们可能会没有它。 但是,当涉及到递归函数调用时,TCE不再是一种优化。 这是一项强制性功能。 这就是为什么在处理递归时,TCE比TCO更好的术语。

尾递归方法和功能

大多数功能语言都实现了TCE。 但是,TCE不足以使每个递归调用成为可能。 要成为TCE的候选人,递归调用必须是方法必须要做的最后一件事。 考虑以下计算列表元素总和的方法:

static Integer sum(List<Integer> list) {return list.isEmpty()? 0: head(list) + sum(tail(list));}

此方法使用head()tail()方法。 请注意,递归调用sum方法并不是该方法要做的最后一件事。 该方法的最后四件事是:

  • 调用head方法
  • 调用tail方法
  • 调用sum方法
  • head的结果和sum的结果sum

即使我们拥有TCE,我们也无法在10,000个元素的列表中使用此方法,因为递归被调用方不在尾部位置。 但是,可以重写此方法以便将求和的调用放在尾部位置:

static Integer sum_(List<Integer> list) {return sumTail(list, 0);
}static Integer sumTail(List<Integer> list, int acc) {return list.isEmpty()? acc: sumTail(tail(list), acc + head(list));
}

现在, sumTail方法是尾递归的,可以通过TCE进行优化。

抽象递归

到目前为止,一切都很好,但是由于Java不实现TCE,为什么还要烦恼所有这些呢? 好吧,Java没有实现它,但是我们可以不用它。 我们需要做的是:

  • 表示未评估的方法调用
  • 将它们存储在类似堆栈的结构中,直到遇到终端条件
  • 以LIFO顺序评估呼叫

递归方法的大多数示例都使用阶乘函数作为示例。 其他使用斐波那契数列示例。 要开始研究,我们将使用更简单的递归加法。

递归和核心递归函数都是函数,其中f(n)f(n‑1)f(n‑2)f(n‑3) ,依此类推,直到遇到终止条件(通常为f(0)f(1) 请记住,在传统编程中,编写通常意味着编写评估结果。 这意味着组成函数f(a)g(a)包括对g(a)求值,然后将结果用作f的输入。 不必那样做。 您可以开发一个compose方法来编写函数,并开发一个higherCompose函数来完成相同的事情。 此方法或此函数均不会评估组成的函数。 它们只会产生另一个功能,以后可以应用。

递归和核心递归相似,但有所不同。 我们创建函数调用列表,而不是函数列表。 使用corecursion,每个步骤都是最终步骤,因此可以对其进行评估以便获得结果并将其用作下一步的输入。 通过递归,我们从另一端开始。 因此,我们必须将未评估的调用放入列表中,直到找到终止条件为止,我们可以根据该条件以相反的顺序处理列表。 换句话说,我们堆叠步骤(不评估它们)直到找到最后一个步骤,然后我们以相反的顺序处理堆叠(后进先出),评估每个步骤并将结果用作下一个输入(实际上是前一个)。

我们遇到的问题是Java为此使用了线程堆栈,并且其容量非常有限。 通常,堆栈将在6,000到7,000个步骤之间溢出。

我们要做的是创建一个返回未评估步骤的函数或方法。 为了表示计算中的步骤,我们将使用一个名为TailCall的抽象类(因为我们希望表示对出现在尾部位置的方法的调用)。

这个TailCall抽象类将有两个子类:一个代表中间调用,当一个步骤的处理被暂停以调用用于评估下一步骤的新方法时。 这将由名为Suspend的类表示。 将使用Supplier<TailCall>>实例化它,它表示下一个递归调用。 这样,我们将把每个尾部调用与下一个尾部链接起来,而不是将所有的TailCalls放入列表中。 这种方法的好处是,这样的链表实际上是一个堆栈,可提供恒定的时间插入以及对最后插入的元素的恒定时间访问,这对于LIFO结构是最佳的。

第二个实现将代表最后一个调用,该调用应返回结果。 因此,我们将其称为Return 。 它不会保存到下一个TailCall的链接,因为接下来没有任何内容,但是它将保存结果。 这是我们得到的:

import java.util.function.Supplier;public abstract class TailCall<T> {public static class Return<T> extends TailCall<T> {private final T t;public Return(T t) {this.t = t;}}public static class Suspend<T> extends TailCall<T> {private final Supplier<TailCall<T>> resume;private Suspend(Supplier<TailCall<T>> resume) {this.resume = resume;}}
}

要处理这些类,我们将需要一些方法:一个返回结果,一个返回下一个调用,以及一个帮助程序方法,确定TailCallSuspend还是Return 。 我们可以避免使用最后一种方法,但是我们必须使用instanceof来完成这项工作,这很丑陋。 这三种方法将是:

public abstract TailCall<T> resume();public abstract T eval();public abstract boolean isSuspend();

resume方法在Return中将没有实现,只会抛出运行时异常。 我们API的用户不应处于调用此方法的情况,因此,如果最终调用该方法,则将是一个错误,并且我们将停止该应用程序。 在Suspend类中,它将返回下一个TailCall

eval方法将返回存储在Return类中的结果。 在我们的第一个版本中,如果在Suspend类上调用它将抛出运行时异常。

isSuspend方法将在Suspend返回true ,在ReturnReturn false 。 清单1显示了第一个版本。

清单1: TailCall抽象类及其两个子类

import java.util.function.Supplier;public abstract class TailCall<T> {public abstract TailCall<T> resume();public abstract T eval();public abstract boolean isSuspend();public static class Return<T> extends TailCall<T> {private final T t;public Return(T t) {this.t = t;}@Overridepublic T eval() {return t;}@Overridepublic boolean isSuspend() {return false;}@Overridepublic TailCall<T> resume() {throw new IllegalStateException("Return has no resume");}}public static class Suspend<T> extends TailCall<T> {private final Supplier<TailCall<T>> resume;public Suspend(Supplier<TailCall<T>> resume) {this.resume = resume;}@Overridepublic T eval() {throw new IllegalStateException("Suspend has no value");}@Overridepublic boolean isSuspend() {return true;}@Overridepublic TailCall<T> resume() {return resume.get();}}
}

现在,要使我们的递归方法可以在任意数量的步骤中工作(在可用内存大小的限制之内!),我们几乎不需要做任何更改。 从我们的原始方法开始:

static int add(int x, int y) {return y == 0? x: add(++x, --y)  ;
}

我们只需要进行清单2中所示的修改即可。

清单2:修改后的递归方法

static TailCall<Integer> add(int x, int y) {  // #1return y == 0? new TailCall.Return<>(x)   // #2: new TailCall.Suspend<>(() -> add(x + 1, y – 1));  // #3
}
  • #1方法现在返回一个TailCall
  • #2在终端条件下,返回Return
  • #3在非终止条件下,返回挂起

现在,我们的方法返回TailCall<Integer>而不是int(#1)。 如果已经达到终止条件,则此返回值可以是Return<Integer> (#2),如果尚未达到,则可以是Suspend<Integer> (#3)。 Return用计算的结果实例化(因为y为0,所以x是x),Suspend用的是Supplier<TailCall<Integer>>实例化,后者是按照执行顺序进行下一步的计算,或者就调用顺序而言,前一个。 重要的是要了解,Return对应于方法调用的最后一步,但对应于评估的第一步。 另请注意,我们对评估进行了少许更改,用x + 1y – 1替换了++x--y 。 这是必要的,因为我们使用的是闭包,仅当对变量的闭包实际上是最终的时才起作用。 这是骗人的,但没有那么多。 我们可以使用原始运算符创建并调用dec和inc这两个方法。

此方法返回的是一连串的TailCall实例,所有实例都是Suspend实例,除了最后一个实例(即Return)。

到目前为止,还不错,但是显然,这种方法并不能代替原始方法。 没有大碍! 原始方法用于:

System.out.println(add(x, y))

我们可以这样使用新方法:

TailCall<Integer> tailCall = add(3, 100000000);while(tailCall .isSuspend()) {tailCall = tailCall.resume();}System.out.println(tailCall.eval());

看起来不是很好吗? 好吧,如果您感到有些沮丧,我可以理解。 您认为我们将以透明的方式使用新方法代替旧方法。 我们似乎离这很远。 但是,我们可以不费吹灰之力就能使事情变得更好。

直接替换堆栈基础递归方法

在上一节的开头,我们说过,递归API的用户将没有机会通过在Return上调用resume或在Suspend上调用eval来弄乱TailCall实例。 通过将评估代码放在Suspend类的eval方法中,可以轻松实现:

public static class Suspend<T> extends TailCall<T> {...@Overridepublic T eval() {TailCall<T> tailRec = this;while(tailRec.isSuspend()) {tailRec = tailRec.resume();}return tailRec.eval();}

现在,我们可以以更简单,更安全的方式获得递归调用的结果:

add(3, 100000000).eval()

但这还不是我们想要的。 我们想要摆脱对eval方法的调用。 这可以通过一个辅助方法来完成:

import static com.fpinjava.excerpt.TailCall.ret;
import static com.fpinjava.excerpt.TailCall.sus;. . .public static int add(int x, int y) {return addRec(x, y).eval();
}private static TailCall<Integer> addRec(int x, int y) {return y == 0? ret(x): sus(() -> addRec(x + 1, y - 1));
}

现在,我们可以完全像原始方法一样调用add方法。 请注意,通过提供静态工厂方法来实例化Return和Suspend,我们使递归API更易于使用:

public static <T> Return<T> ret(T t) {return new Return<>(t);
}public static <T> Suspend<T> sus(Supplier<TailCall<T>> s) {return new Suspend<>(s);
}

清单3显示了完整的TailCall类。 我们添加了一个私有的no arg构造函数,以防止被其他类扩展。

清单3:完整的TailCall

package com.fpinjava.excerpt;import java.util.function.Supplier;public abstract class TailCall<T> {public abstract TailCall<T> resume();public abstract T eval();public abstract boolean isSuspend();private TailCall() {}public static class Return<T> extends TailCall<T> {private final T t;private Return(T t) {this.t = t;}@Overridepublic T eval() {return t;}@Overridepublic boolean isSuspend() {return false;}@Overridepublic TailCall<T> resume() {throw new IllegalStateException("Return has no resume");}}public static class Suspend<T> extends TailCall<T> {private final Supplier<TailCall<T>> resume;private Suspend(Supplier<TailCall<T>> resume) {this.resume = resume;}@Overridepublic T eval() {TailCall<T> tailRec = this;while(tailRec.isSuspend()) {tailRec = tailRec.resume();}return tailRec.eval();}@Overridepublic boolean isSuspend() {return true;}@Overridepublic TailCall<T> resume() {return resume.get();}}public static <T> Return<T> ret(T t) {return new Return<>(t);}public static <T> Suspend<T> sus(Supplier<TailCall<T>> s) {return new Suspend<>(s);}
}

既然有了堆栈安全的尾部递归方法,就可以对函数执行相同的操作吗? 在我的《 Java中的函数式编程》一书中,我谈到了如何做到这一点。

翻译自: https://www.javacodegeeks.com/2015/10/stack-safe-recursion-in-java.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/357086.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数字化经营支付3.0有哪些盈利机会

实体商户的现状 服务商经营现状 官方政策支持 微信平台通过多渠道助力商户发放 优惠券,如扫码领券、API领券、支 付有礼、附近3公里等形式,给商户 做引流拓客。

nessus重置密码

许久不用的nessus密码居然忘记了&#xff0c;查了下&#xff1a; cmd下进入到nessus的安装目录 提升为管理员&#xff0c;登录系统 如果想用之前的账号&#xff0c;可以直接在系统内重置密码。转载于:https://www.cnblogs.com/nayu/p/5640527.html

程序员有哪些可以写博客的网站?

俗话说&#xff1a;好记性不如烂笔头&#xff0c;作为一名合格的程序员还是抽时间写写博客的&#xff0c;马云曾说“成功的人有两个特质&#xff0c;一个是喜欢写作&#xff0c;一个是喜欢演讲”&#xff0c;我觉得吧&#xff0c;写作一是为了总结&#xff0c;二是督促自己成长…

工厂方法设计模式示例

本文是我们名为“ Java设计模式 ”的学院课程的一部分。 在本课程中&#xff0c;您将深入研究大量的设计模式&#xff0c;并了解如何在Java中实现和利用它们。 您将了解模式如此重要的原因&#xff0c;并了解何时以及如何应用模式中的每一个。 在这里查看 &#xff01; 目录 …

jmx jolokia_使用Jolokia和JMX进行客户端服务器监视

jmx jolokiaJava监视工具的选择非常广泛&#xff08;由Google提供的随机选择和顺序&#xff09;&#xff1a; javamelody 压力探头 JVisualVM 控制台 贾蒙 Java JMX Nagios插件不适用 此外&#xff0c;还有各种专用工具&#xff0c;例如ActiveMQ &#xff0c; JBoss &…

利用 %20 替换 空格

将字符串中的空格都替换为 %20 &#xff08; 时间复杂度为O&#xff08;N&#xff09;的解法 &#xff09; void ReplaceBlankSpace(char* arr){if (arr){int count 0;int lenth strlen(arr);for (int i 0; i < lenth;i)if (arr[i] )count;char*before arrlenth, *beh…

Node.js 官方文档中文版

这目录也是醉了 。 列出跟没列出没两样 转载于:https://www.cnblogs.com/ganmk--jy/p/5646860.html

Smaug Coverage

转载于:https://www.cnblogs.com/edisonxiang/p/5650656.html

图谱(学习地图)系列总结,持续更新中

目录 1.2022年最新前端学习路线图 2.2022年最新大数据学习路线图 3.2022年最新javaEE学习路线图 4.2022年最新UI/UE学习路线图 5.2022年java学习路线指南 6.JavaScript学习总结&#xff08;思维导图篇&#xff09; 7.思维脑图——数据分析实战&#xff08;最新版&#…

ccxt k线数据_寻找相似的历史k线

有网友提问应该用什么样的数据库/数据结构/算法来计算某支股票的相似K线? 具体的问题描述是&#xff0c;假设给出某股某段行情K线&#xff08;单位/日&#xff09;&#xff0c;从任何其他股票历史中匹配出与之最为相似的某段历史K线&#xff0c;并给出相似度值&#xff08;单位…

k均值例子 数据挖掘_数据挖掘的技术有很多种,常用的数据挖掘技术就这13种...

数据挖掘就是从大量的、不完全的、有噪声的、模糊的、随机的数据中&#xff0c;提取隐含在其中的、人们事先不知道的但又是潜在有用的信息和知识的过程。数据挖掘的任务是从数据集中发现模式&#xff0c;可以发现的模式有很多种&#xff0c;按功能可以分为两大类&#xff1a;预…

【TypeScript系列教程05】保留关键字

目录 TypeScript 保留关键字 TypeScript 保留关键字 TypeScript 保留关键字如下表所示&#xff1a;

中国剩余定理 互质与非互质版本

中国剩余定理互质版 设m1&#xff0c;m2&#xff0c;m3&#xff0c;...&#xff0c;mk是两两互素的正整数&#xff0c;即gcd(mi,mj)1&#xff0c;i!j&#xff0c;i,j1,2,3,...,k. 则同余方程组&#xff1a; x a1 (mod n1) x a2 (mod n2) ... x ak (mod nk) 模[n1,n2,...nk]有…

aix 的c库为什么都是静态库_卢卡库:若梅罗、莱万都在努力突破极限,为什么我不能做到呢...

直播吧12月8日讯 北京时间周四凌晨的欧冠小组赛末轮比赛&#xff0c;国米将在主场迎战顿涅茨克矿工。目前&#xff0c;蓝黑军团依然保留着些许出线希望。在这场比赛之前&#xff0c;国米射手卢卡库接受了《法国足球》的采访&#xff0c;他谈论了和孔蒂的关系、和劳塔罗的搭档以…

便携式不锈钢管道焊接机器人_304不锈钢管居然可以发黑?

大家好&#xff0c;我是圣联达不锈钢管材管件孙立成。今天有伙伴们问我&#xff0c;自己使用的304不锈钢管表面怎么发黑了。带着这个问题&#xff0c;今天我特意去梳理了有关304发黑的缘故及其解决方法。304不锈钢管实际上按照正常生产流程&#xff0c;好的304不锈钢管加工制做…

jrockit_Java堆空间– JRockit和IBM VM

jrockit本文将为您提供JRockit Java堆空间与HotSpot VM的概述。 它还将为您提供有关JRockit和HotSpot的Oracle未来计划的一些背景知识。 Oracle JRockit VM Java堆&#xff1a;2个不同的内存空间 -Java堆&#xff08;YoungGen和OldGen&#xff09; -本机内存空间&#xff08;…

检验例题_高考必考|化学工艺流程之物质的分离提纯及检验鉴别,轻松拿分

嗨&#xff0c;小器来喽&#xff01;今天给大家带来的资料依旧是化学的工艺流程的相关知识&#xff0c;因为小器知道在这部分内容中大家是很容易丢分的&#xff0c;都是基础知识不扎实的原因&#xff0c;所以给大家的资料都是从易到难的安排还有专属高考相关例题&#xff0c;简…

三态门三个状态vhdl_温故知新,高阻态和三态门

欢迎FPGA工程师加入官方微信技术群高阻态高阻态这是一个数字电路里常见的术语&#xff0c;指的是电路的一种输出状态&#xff0c;既不是高电平也不是低电平&#xff0c;如果高阻态再输入下一级电路的话&#xff0c;对下级电路无任何影响&#xff0c;和没接一样&#xff0c;如果…

口译员设计模式示例

本文是我们名为“ Java设计模式 ”的学院课程的一部分。 在本课程中&#xff0c;您将深入研究大量的设计模式&#xff0c;并了解如何在Java中实现和利用它们。 您将了解模式如此重要的原因&#xff0c;并了解何时以及如何应用模式中的每一个。 在这里查看 &#xff01; 目录 …

「Unity」UGUI的Text实现首行缩进的办法

我的Unity版本&#xff1a;5.3.5f1 直接说正事 Unity的Text组件&#xff0c;想实现代码拿到一段话&#xff0c;在前面加个缩进&#xff0c;让之后的字符依次后移。 有人觉得很简单&#xff0c;然后就这样写了&#xff1a; 但是尝试过的人就会发现&#xff0c;如果用代码在最前…