簡體   English   中英

遞歸方法調用在 kotlin 中導致 StackOverFlowError 但在 java 中不會

[英]Recursive method call cause StackOverFlowError in kotlin but not in java

我在 java 和 kotlin 中有兩個幾乎相同的代碼

爪哇:

public void reverseString(char[] s) {
    helper(s, 0, s.length - 1);
}

public void helper(char[] s, int left, int right) {
    if (left >= right) return;
    char tmp = s[left];
    s[left++] = s[right];
    s[right--] = tmp;
    helper(s, left, right);
}

科特林:

fun reverseString(s: CharArray): Unit {
    helper(0, s.lastIndex, s)
}

fun helper(i: Int, j: Int, s: CharArray) {
    if (i >= j) {
        return
    }
    val t = s[j]
    s[j] = s[i]
    s[i] = t
    helper(i + 1, j - 1, s)
}

java 代碼通過了大量輸入的測試,但 kotlin 代碼會導致StackOverFlowError除非我在 kotlin 中的helper函數之前添加了tailrec關鍵字。

我想知道為什么這個函數在 java 和帶有tailrec中有效,但在沒有tailrec

PS:我知道tailrec做什么

我想知道為什么這個功能在Java中工作,並在科特林tailrec但不是在科特林沒有tailrec

簡短的回答是因為您的Kotlin方法比JAVA方法“更重”。 在每次調用時,它都會調用另一個“引發” StackOverflowError 因此,請參閱下面更詳細的說明。

reverseString() Java 字節碼等價物

我相應地在KotlinJAVA 中檢查了您的方法的字節碼:

JAVA 中的 Kotlin 方法字節碼

...
public final void reverseString(@NotNull char[] s) {
    Intrinsics.checkParameterIsNotNull(s, "s");
    this.helper(0, ArraysKt.getLastIndex(s), s);
}

public final void helper(int i, int j, @NotNull char[] s) {
    Intrinsics.checkParameterIsNotNull(s, "s");
    if (i < j) {
        char t = s[j];
        s[j] = s[i];
        s[i] = t;
        this.helper(i + 1, j - 1, s);
    }
}
...

JAVA中的JAVA方法字節碼

...
public void reverseString(char[] s) {
    this.helper(s, 0, s.length - 1);
}

public void helper(char[] s, int left, int right) {
    if (left < right) {
        char temp = s[left];
        s[left++] = s[right];
        s[right--] = temp;
        this.helper(left, right, s);
    }
}
...

因此,有兩個主要區別:

  1. Kotlin版本中為每個helper()調用Intrinsics.checkParameterIsNotNull(s, "s")
  2. JAVA方法中的左右索引會遞增,而在Kotlin中,每次遞歸調用都會創建新索引。

因此,讓我們測試Intrinsics.checkParameterIsNotNull(s, "s")單獨影響行為。

測試兩種實現

我為這兩種情況創建了一個簡單的測試:

@Test
public void testJavaImplementation() {
    char[] chars = new char[20000];
    new Example().reverseString(chars);
}

@Test
fun testKotlinImplementation() {
    val chars = CharArray(20000)
    Example().reverseString(chars)
}

對於JAVA ,測試成功而沒有問題,而對於Kotlin ,由於StackOverflowError失敗了。 但是,在我將Intrinsics.checkParameterIsNotNull(s, "s")JAVA方法后,它也失敗了:

public void helper(char[] s, int left, int right) {
    Intrinsics.checkParameterIsNotNull(s, "s"); // add the same call here

    if (left >= right) return;
    char tmp = s[left];
    s[left] = s[right];
    s[right] = tmp;
    helper(s, left + 1, right - 1);
}

結論

您的Kotlin方法具有較小的遞歸深度,因為它在每一步調用Intrinsics.checkParameterIsNotNull(s, "s") ,因此比其JAVA對應方法更重。 如果您不想要這種自動生成的方法,那么您可以在編譯期間禁用空檢查,如回答here

但是,由於您了解tailrec帶來的好處(將您的遞歸調用轉換為迭代調用),您應該使用它。

Kotlin 只需要一點點堆棧飢餓(Int object params io int params)。 除了適合此處的 tailrec 解決方案之外,您還可以通過異或運算消除局部變量temp

fun helper(i: Int, j: Int, s: CharArray) {
    if (i >= j) {
        return
    }               // i: a          j: b
    s[j] ^= s[i]    //               j: a^b
    s[i] ^= s[j]    // i: a^a^b == b
    s[j] ^= s[i]    //               j: a^b^b == a
    helper(i + 1, j - 1, s)
}

不完全確定這是否適用於刪除局部變量。

同樣消除 j 可能會:

fun reverseString(s: CharArray): Unit {
    helper(0, s)
}

fun helper(i: Int, s: CharArray) {
    if (i >= s.lastIndex - i) {
        return
    }
    val t = s[s.lastIndex - i]
    s[s.lastIndex - i] = s[i]
    s[i] = t
    helper(i + 1, s)
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM