递归方法调用在 kotlin 中导致 StackOverFlowError 但在 java 中不会

ham*_*d_c 13 java recursion kotlin

我在 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);
}
Run Code Online (Sandbox Code Playgroud)

科特林:

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)
}
Run Code Online (Sandbox Code Playgroud)

java 代码通过了大量输入的测试,但 kotlin 代码导致 aStackOverFlowError除非我在 kotlin 中tailrechelper函数之前添加关键字。

我想知道为什么这个函数在 java 和 kolin 中工作,tailrec但在 kotlin 中没有tailrec

PS: 我知道该怎么tailrec

Ana*_*lii 7

我想知道为什么这个函数在javakotlin中都有效tailrec但在kotlin 中没有tailrec

简短的回答是因为您的Kotlin方法比JAVA方法“更重” 。在每次调用时,它都会调用另一个“激发”的方法StackOverflowError。因此,请参阅下面更详细的说明。

Java字节码等价物 reverseString()

我相应地在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);
    }
}
...
Run Code Online (Sandbox Code Playgroud)

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);
    }
}
...
Run Code Online (Sandbox Code Playgroud)

因此,有两个主要区别:

  1. Intrinsics.checkParameterIsNotNull(s, "s")helper()Kotlin版本中为每个调用。
  2. JAVA方法中的左右索引会递增,而在Kotlin中,每次递归调用都会创建新索引。

所以,让我们测试一下Intrinsics.checkParameterIsNotNull(s, "s")单独如何影响行为。

测试两种实现

我为这两种情况创建了一个简单的测试:

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

@Test
fun testKotlinImplementation() {
    val chars = CharArray(20000)
    Example().reverseString(chars)
}
Run Code Online (Sandbox Code Playgroud)

对于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);
}
Run Code Online (Sandbox Code Playgroud)

结论

您的Kotlin方法具有较小的递归深度,因为它Intrinsics.checkParameterIsNotNull(s, "s")在每一步都会调用,因此比JAVA方法重。如果您不想要这种自动生成的方法,那么您可以在编译期间禁用空检查,如回答here

但是,由于您了解tailrec带来的好处(将您的递归调用转换为迭代调用),您应该使用它。