日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > java >内容正文

java

Java中的堆栈安全递归

發(fā)布時(shí)間:2023/12/3 java 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java中的堆栈安全递归 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

在本文中,摘自《 Java中的函數(shù)式編程 》一書,我解釋了如何使用遞歸,同時(shí)避免了StackOverflow異常的風(fēng)險(xiǎn)。

Corecursion正在使用第一步的輸出作為下一步的輸入來構(gòu)成計(jì)算步驟。 遞歸是相同的操作,但是從最后一步開始。 在這種情況下,我們必須延遲評(píng)估,直到遇到基本條件(與corecursion的第一步相對(duì)應(yīng))為止。

假設(shè)我們的編程語言中只有兩條指令:遞增(向值加1)和遞減(從值中減去1)。 讓我們通過編寫這些指令來實(shí)現(xiàn)加法。

Corecursive和遞歸加法示例

為了將兩個(gè)數(shù)字x和y相加,我們可以執(zhí)行以下操作:

  • 如果y == 0 ,則返回x
  • 否則,遞增x ,遞減y ,然后重新開始。

這可以用Java編寫為:

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

或更簡(jiǎn)單:

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

注意,直接使用參數(shù)x和y沒問題,因?yàn)樵贘ava中,所有參數(shù)都是按值傳遞的。 另請(qǐng)注意,我們已使用后減量來簡(jiǎn)化編碼。 但是,我們可以通過稍微改變條件來使用預(yù)減量,從而將形式從y迭代為1到將y?1迭代為0 :

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

遞歸版本比較棘手,但仍然非常簡(jiǎn)單:

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

兩種方法似乎都可行,但是如果我們嘗試使用大量的遞歸版本,可能會(huì)感到驚訝。 雖然

addRec(10000, 3);

切換參數(shù),產(chǎn)生預(yù)期結(jié)果10003,如下所示:

addRec(3, 10000);

產(chǎn)生一個(gè)StackOverflowException 。

如何用Java實(shí)現(xiàn)遞歸?

要了解正在發(fā)生的事情,我們必須查看Java如何處理方法調(diào)用。 調(diào)用方法時(shí),Java會(huì)掛起當(dāng)前正在執(zhí)行的操作,并將環(huán)境壓入堆棧以為調(diào)用的方法執(zhí)行留出空間。 當(dāng)此方法返回時(shí),Java彈出堆棧以恢復(fù)環(huán)境并恢復(fù)程序執(zhí)行。 如果我們依次調(diào)用一個(gè)方法,則堆棧將始終保存這些方法調(diào)用環(huán)境中的至少一個(gè)。

但是方法不僅是通過一個(gè)接一個(gè)地調(diào)用它們而構(gòu)成的。 方法調(diào)用方法。 如果method1作為其實(shí)現(xiàn)的一部分調(diào)用method2 ,則Java會(huì)再次掛起method1執(zhí)行,將當(dāng)前環(huán)境壓入stack ,然后開始執(zhí)行method2 。 當(dāng)method2返回時(shí),Java從堆棧中彈出最后推送的環(huán)境并恢復(fù)執(zhí)行(在本例中為method1 )。 當(dāng)method1完畢后,Java從棧中彈出一次,并恢復(fù)它在調(diào)用此方法之前做的事情。

當(dāng)然,方法調(diào)用可能嵌套得很深。 方法嵌套深度是否有限制? 是。 限制是堆棧的大小。 在當(dāng)前情況下,該限制約為幾千個(gè)級(jí)別,盡管可以通過配置堆棧大小來增加此限制。 但是,所有線程都使用相同的堆棧大小,因此增加單個(gè)計(jì)算的堆棧大小通常會(huì)浪費(fèi)空間。 默認(rèn)堆棧大小在320k和1024k之間變化,具體取決于Java版本和所使用的系統(tǒng)。 對(duì)于具有最小堆棧使用率的64位Java 8程序,嵌套方法調(diào)用的最大數(shù)量約為7000。通常,除了非常特殊的情況外,我們不需要更多的嵌套方法調(diào)用。 一種這樣的情況是遞歸方法調(diào)用。

消除尾調(diào)用(TCE)似乎有必要將環(huán)境推送到堆棧上,以便允許在被調(diào)用方法返回后恢復(fù)計(jì)算。 但不總是。 如果對(duì)方法的調(diào)用是調(diào)用方法中的最后一件事,則返回時(shí)沒有任何恢復(fù)操作,因此可以直接與當(dāng)前方法的調(diào)用者而不是當(dāng)前方法本身一起恢復(fù)。 在最后一個(gè)位置發(fā)生的方法調(diào)用(即返回之前的最后一件事)稱為tail call 。 避免在尾部調(diào)用之后將環(huán)境壓入堆棧以恢復(fù)方法處理是一種稱為尾部消除(TCE)的優(yōu)化技術(shù)。 不幸的是,Java沒有實(shí)現(xiàn)TCE。

消除尾聲有時(shí)被稱為尾聲優(yōu)化(TCO)。 TCE通常是一種優(yōu)化,我們可能會(huì)沒有它。 但是,當(dāng)涉及到遞歸函數(shù)調(diào)用時(shí),TCE不再是一種優(yōu)化。 這是一項(xiàng)強(qiáng)制性功能。 這就是為什么在處理遞歸時(shí),TCE比TCO更好的術(shù)語。

尾遞歸方法和功能

大多數(shù)功能語言都實(shí)現(xiàn)了TCE。 但是,TCE不足以使每個(gè)遞歸調(diào)用成為可能。 要成為TCE的候選人,遞歸調(diào)用必須是方法必須要做的最后一件事。 考慮以下計(jì)算列表元素總和的方法:

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

此方法使用head()和tail()方法。 請(qǐng)注意,遞歸調(diào)用sum方法并不是該方法要做的最后一件事。 該方法的最后四件事是:

  • 調(diào)用head方法
  • 調(diào)用tail方法
  • 調(diào)用sum方法
  • 將head的結(jié)果和sum的結(jié)果sum

即使我們擁有TCE,我們也無法在10,000個(gè)元素的列表中使用此方法,因?yàn)檫f歸被調(diào)用方不在尾部位置。 但是,可以重寫此方法以便將求和的調(diào)用放在尾部位置:

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)); }

現(xiàn)在, sumTail方法是尾遞歸的,可以通過TCE進(jìn)行優(yōu)化。

抽象遞歸

到目前為止,一切都很好,但是由于Java不實(shí)現(xiàn)TCE,為什么還要煩惱所有這些呢? 好吧,Java沒有實(shí)現(xiàn)它,但是我們可以不用它。 我們需要做的是:

  • 表示未評(píng)估的方法調(diào)用
  • 將它們存儲(chǔ)在類似堆棧的結(jié)構(gòu)中,直到遇到終端條件
  • 以LIFO順序評(píng)估呼叫

遞歸方法的大多數(shù)示例都使用階乘函數(shù)作為示例。 其他使用斐波那契數(shù)列示例。 要開始研究,我們將使用更簡(jiǎn)單的遞歸加法。

遞歸和核心遞歸函數(shù)都是函數(shù),其中f(n)是f(n?1) , f(n?2) , f(n?3) ,依此類推,直到遇到終止條件(通常為f(0)的f(1) 請(qǐng)記住,在傳統(tǒng)編程中,編寫通常意味著編寫評(píng)估結(jié)果。 這意味著組成函數(shù)f(a)和g(a)包括對(duì)g(a)求值,然后將結(jié)果用作f的輸入。 不必那樣做。 您可以開發(fā)一個(gè)compose方法來編寫函數(shù),并開發(fā)一個(gè)higherCompose函數(shù)來完成相同的事情。 此方法或此函數(shù)均不會(huì)評(píng)估組成的函數(shù)。 它們只會(huì)產(chǎn)生另一個(gè)功能,以后可以應(yīng)用。

遞歸和核心遞歸相似,但有所不同。 我們創(chuàng)建函數(shù)調(diào)用列表,而不是函數(shù)列表。 使用corecursion,每個(gè)步驟都是最終步驟,因此可以對(duì)其進(jìn)行評(píng)估以便獲得結(jié)果并將其用作下一步的輸入。 通過遞歸,我們從另一端開始。 因此,我們必須將未評(píng)估的調(diào)用放入列表中,直到找到終止條件為止,我們可以根據(jù)該條件以相反的順序處理列表。 換句話說,我們堆疊步驟(不評(píng)估它們)直到找到最后一個(gè)步驟,然后我們以相反的順序處理堆疊(后進(jìn)先出),評(píng)估每個(gè)步驟并將結(jié)果用作下一個(gè)輸入(實(shí)際上是前一個(gè))。

我們遇到的問題是Java為此使用了線程堆棧,并且其容量非常有限。 通常,堆棧將在6,000到7,000個(gè)步驟之間溢出。

我們要做的是創(chuàng)建一個(gè)返回未評(píng)估步驟的函數(shù)或方法。 為了表示計(jì)算中的步驟,我們將使用一個(gè)名為TailCall的抽象類(因?yàn)槲覀兿M硎緦?duì)出現(xiàn)在尾部位置的方法的調(diào)用)。

這個(gè)TailCall抽象類將有兩個(gè)子類:一個(gè)代表中間調(diào)用,當(dāng)一個(gè)步驟的處理被暫停以調(diào)用用于評(píng)估下一步驟的新方法時(shí)。 這將由名為Suspend的類表示。 將使用Supplier<TailCall>>實(shí)例化它,它表示下一個(gè)遞歸調(diào)用。 這樣,我們將把每個(gè)尾部調(diào)用與下一個(gè)尾部鏈接起來,而不是將所有的TailCalls放入列表中。 這種方法的好處是,這樣的鏈表實(shí)際上是一個(gè)堆棧,可提供恒定的時(shí)間插入以及對(duì)最后插入的元素的恒定時(shí)間訪問,這對(duì)于LIFO結(jié)構(gòu)是最佳的。

第二個(gè)實(shí)現(xiàn)將代表最后一個(gè)調(diào)用,該調(diào)用應(yīng)返回結(jié)果。 因此,我們將其稱為Return 。 它不會(huì)保存到下一個(gè)TailCall的鏈接,因?yàn)榻酉聛頉]有任何內(nèi)容,但是它將保存結(jié)果。 這是我們得到的:

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;}} }

要處理這些類,我們將需要一些方法:一個(gè)返回結(jié)果,一個(gè)返回下一個(gè)調(diào)用,以及一個(gè)幫助程序方法,確定TailCall是Suspend還是Return 。 我們可以避免使用最后一種方法,但是我們必須使用instanceof來完成這項(xiàng)工作,這很丑陋。 這三種方法將是:

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

resume方法在Return中將沒有實(shí)現(xiàn),只會(huì)拋出運(yùn)行時(shí)異常。 我們API的用戶不應(yīng)處于調(diào)用此方法的情況,因此,如果最終調(diào)用該方法,則將是一個(gè)錯(cuò)誤,并且我們將停止該應(yīng)用程序。 在Suspend類中,它將返回下一個(gè)TailCall 。

eval方法將返回存儲(chǔ)在Return類中的結(jié)果。 在我們的第一個(gè)版本中,如果在Suspend類上調(diào)用它將拋出運(yùn)行時(shí)異常。

isSuspend方法將在Suspend返回true ,在Return中Return false 。 清單1顯示了第一個(gè)版本。

清單1: TailCall抽象類及其兩個(gè)子類

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();}} }

現(xiàn)在,要使我們的遞歸方法可以在任意數(shù)量的步驟中工作(在可用內(nèi)存大小的限制之內(nèi)!),我們幾乎不需要做任何更改。 從我們的原始方法開始:

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

我們只需要進(jìn)行清單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方法現(xiàn)在返回一個(gè)TailCall
  • #2在終端條件下,返回Return
  • #3在非終止條件下,返回掛起

現(xiàn)在,我們的方法返回TailCall<Integer>而不是int(#1)。 如果已經(jīng)達(dá)到終止條件,則此返回值可以是Return<Integer> (#2),如果尚未達(dá)到,則可以是Suspend<Integer> (#3)。 Return用計(jì)算的結(jié)果實(shí)例化(因?yàn)閥為0,所以x是x),Suspend用的是Supplier<TailCall<Integer>>實(shí)例化,后者是按照?qǐng)?zhí)行順序進(jìn)行下一步的計(jì)算,或者就調(diào)用順序而言,前一個(gè)。 重要的是要了解,Return對(duì)應(yīng)于方法調(diào)用的最后一步,但對(duì)應(yīng)于評(píng)估的第一步。 另請(qǐng)注意,我們對(duì)評(píng)估進(jìn)行了少許更改,用x + 1和y – 1替換了++x和--y 。 這是必要的,因?yàn)槲覀兪褂玫氖情]包,僅當(dāng)對(duì)變量的閉包實(shí)際上是最終的時(shí)才起作用。 這是騙人的,但沒有那么多。 我們可以使用原始運(yùn)算符創(chuàng)建并調(diào)用dec和inc這兩個(gè)方法。

此方法返回的是一連串的TailCall實(shí)例,所有實(shí)例都是Suspend實(shí)例,除了最后一個(gè)實(shí)例(即Return)。

到目前為止,還不錯(cuò),但是顯然,這種方法并不能代替原始方法。 沒有大礙! 原始方法用于:

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

我們可以這樣使用新方法:

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

看起來不是很好嗎? 好吧,如果您感到有些沮喪,我可以理解。 您認(rèn)為我們將以透明的方式使用新方法代替舊方法。 我們似乎離這很遠(yuǎn)。 但是,我們可以不費(fèi)吹灰之力就能使事情變得更好。

直接替換堆?;A(chǔ)遞歸方法

在上一節(jié)的開頭,我們說過,遞歸API的用戶將沒有機(jī)會(huì)通過在Return上調(diào)用resume或在Suspend上調(diào)用eval來弄亂TailCall實(shí)例。 通過將評(píng)估代碼放在Suspend類的eval方法中,可以輕松實(shí)現(xiàn):

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

現(xiàn)在,我們可以以更簡(jiǎn)單,更安全的方式獲得遞歸調(diào)用的結(jié)果:

add(3, 100000000).eval()

但這還不是我們想要的。 我們想要擺脫對(duì)eval方法的調(diào)用。 這可以通過一個(gè)輔助方法來完成:

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)); }

現(xiàn)在,我們可以完全像原始方法一樣調(diào)用add方法。 請(qǐng)注意,通過提供靜態(tài)工廠方法來實(shí)例化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類。 我們添加了一個(gè)私有的no arg構(gòu)造函數(shù),以防止被其他類擴(kuò)展。

清單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);} }

既然有了堆棧安全的尾部遞歸方法,就可以對(duì)函數(shù)執(zhí)行相同的操作嗎? 在我的《 Java中的函數(shù)式編程》一書中,我談到了如何做到這一點(diǎn)。

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

總結(jié)

以上是生活随笔為你收集整理的Java中的堆栈安全递归的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。