递归方法调用导致在Kotlin中出现StackOverFlowError但在Java中却没有


14

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

科特林:

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中tailrechelper函数之前添加了关键字。

我想知道为什么此函数在java中以及在带有tailreckolin的情况下也可以在但没有kotlin的情况下在tailrec吗?

PS: 我知道该怎么tailrec


1
当我对它们进行测试时,我发现Java版本适用于大约29500的数组大小,但是Kotlin版本可以在18500左右停下来。这是一个很大的差异,但不是很大。如果您需要将其用于大型数组,则唯一的好解决方案是使用tailrec或避免递归;可用的堆栈大小在运行之间,JVM和设置之间以及根据方法及其参数而有所不同。但是,如果您出于纯粹的好奇心而问(一个很好的理由!),那么我不确定。您可能需要查看字节码。
gidds

Answers:


7

我想知道为什么这个功能在工作的Java以及在科特林tailrec,但不是在科特林没有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);
    }
}
...

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

因此,有2个主要区别:

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

@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方法要重。如果您不希望使用此自动生成的方法,则可以在编译过程中禁用null检查,如此处所示

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


@ user207421每个方法调用都有自己的堆栈框架,包括Intrinsics.checkParameterIsNotNull(...)。显然,每一个这样的堆栈帧需要一定量的存储器(用于LocalVariableTable和操作数堆栈上等等)..
Anatolii

0

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)
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.