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

2022-09-04 04:03:44

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

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

Java代码以巨大的输入通过了测试,但kotlin代码导致除非我在kotlin中的函数之前添加了关键字。StackOverFlowErrortailrechelper

我想知道为什么这个函数在java中工作,也可以在kolin中使用,但在kotlin中没有?tailrectailrec

附言:我知道该怎么做tailrec


答案 1

我想知道为什么这个函数在java中工作,也可以在kotlin中使用,但在kotlin中没有?tailrectailrec

简短的答案是因为您的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")Kotlin 版本中,为每个版本调用。helper()
  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,由于.但是,在我添加到JAVA方法之后,它也失败了:StackOverflowErrorIntrinsics.checkParameterIsNotNull(s, "s")

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 方法具有较小的递归深度,因为它在每一步都调用,因此比 JAVA 对应物更重。如果您不想要这种自动生成的方法,则可以在编译期间禁用空检查,如此处所述Intrinsics.checkParameterIsNotNull(s, "s")

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


答案 2

Kotlin 只是稍微多了一点堆栈饥渴(Int 对象参数 i.o. int 参数)。除了适合此处的 tailrec 解决方案之外,您还可以通过异或 ing 来消除局部变量: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)
}

推荐