简体   繁体   English

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

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

I have two almost identical code in java and kotlin我在 java 和 kotlin 中有两个几乎相同的代码

Java:爪哇:

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

Kotlin:科特林:

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

The java code pass the test with a huge input but the kotlin code cause a StackOverFlowError unless i added tailrec keyword before the helper function in kotlin. java 代码通过了大量输入的测试,但 kotlin 代码会导致StackOverFlowError除非我在 kotlin 中的helper函数之前添加了tailrec关键字。

I want to know why this function works in java and also in kolin with tailrec but not in kotlin without tailrec ?我想知道为什么这个函数在 java 和带有tailrec中有效,但在没有tailrec

PS: i know what tailrec do PS:我知道tailrec做什么

I want to know why this function works in java and also in kotlin with tailrec but not in kotlin without tailrec ?我想知道为什么这个功能在Java中工作,并在科特林tailrec但不是在科特林没有tailrec

The short answer is because your Kotlin method is "heavier" than the JAVA one.简短的回答是因为您的Kotlin方法比JAVA方法“更重”。 At every call it calls another method that "provokes" StackOverflowError .在每次调用时,它都会调用另一个“引发” StackOverflowError So, see a more detailed explanation below.因此,请参阅下面更详细的说明。

Java bytecode equivalents for reverseString() reverseString() Java 字节码等价物

I checked the byte code for your methods in Kotlin and JAVA correspondingly:我相应地在KotlinJAVA 中检查了您的方法的字节码:

Kotlin method bytecode in JAVA 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 method bytecode in JAVA 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);
    }
}
...

So, there're 2 main differences:因此,有两个主要区别:

  1. Intrinsics.checkParameterIsNotNull(s, "s") is invoked for each helper() in the Kotlin version.Kotlin版本中为每个helper()调用Intrinsics.checkParameterIsNotNull(s, "s")
  2. Left and right indices in JAVA method get incremented, while in Kotlin new indices are created for each recursive call. JAVA方法中的左右索引会递增,而在Kotlin中,每次递归调用都会创建新索引。

So, let's test how Intrinsics.checkParameterIsNotNull(s, "s") alone affects the behavior.因此,让我们测试Intrinsics.checkParameterIsNotNull(s, "s")单独影响行为。

Test both implementations测试两种实现

I've created a simple test for both cases:我为这两种情况创建了一个简单的测试:

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

And

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

For JAVA the test succeeded without problems while for Kotlin it failed miserably due to a StackOverflowError .对于JAVA ,测试成功而没有问题,而对于Kotlin ,由于StackOverflowError失败了。 However, after I added Intrinsics.checkParameterIsNotNull(s, "s") to the JAVA method it failed as well:但是,在我将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);
}

Conclusion结论

Your Kotlin method has a smaller recursion depth as it invokes Intrinsics.checkParameterIsNotNull(s, "s") at every step and thus is heavier than its JAVA counterpart.您的Kotlin方法具有较小的递归深度,因为它在每一步调用Intrinsics.checkParameterIsNotNull(s, "s") ,因此比其JAVA对应方法更重。 If you don't want this auto-generated method then you can disable null checks during compilation as answered here如果您不想要这种自动生成的方法,那么您可以在编译期间禁用空检查,如回答here

However, since you understand what benefit tailrec brings (converts your recursive call into an iterative one) you should use that one.但是,由于您了解tailrec带来的好处(将您的递归调用转换为迭代调用),您应该使用它。

Kotlin is just a tiny bit more stack hungry (Int object params io int params). Kotlin 只需要一点点堆栈饥饿(Int object params io int params)。 Besides the tailrec solution which fits here, you can eliminate the local variable temp by xor-ing:除了适合此处的 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)
}

Not entirely sure whether this works to remove a local variable.不完全确定这是否适用于删除局部变量。

Also eliminating j might do:同样消除 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