递归方法调用在kotlin中会导致StackOverflower错误,但在java中不会

递归方法调用在kotlin中会导致StackOverflower错误,但在java中不会,java,recursion,kotlin,Java,Recursion,Kotlin,我在java和kotlin中有两个几乎相同的代码 Java: Kotlin: java代码以巨大的输入通过了测试,但kotlin代码会导致StackOverflowerError,除非我在kotlin中的helper函数之前添加了tailrec关键字 我想知道为什么这个函数在java和kolin中使用tailrec,但在kotlin中不使用tailrec p.S: 我知道tailrec做什么 我想知道为什么这个函数在java和kotlin中使用tailrec,但在kotlin中不使用tailre

我在java和kotlin中有两个几乎相同的代码

Java:

Kotlin:

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

我想知道为什么这个函数在java和kolin中使用
tailrec
,但在kotlin中不使用
tailrec

p.S: 我知道tailrec做什么

我想知道为什么这个函数在javakotlin中使用
tailrec
,但在kotlin中不使用
tailrec

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

reverseString()的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)
}
我在KotlinJAVA中相应地检查了方法的字节码:

JAVA中的Kotlin方法字节码

对于JAVA来说,测试成功了,没有出现问题;而对于Kotlin来说,由于
堆栈溢出错误,测试失败得很惨。但是,在我向JAVA方法添加
Intrinsics.checkParametersNotnull(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方法在调用
Intrinsics时具有较小的递归深度。在每个步骤中检查ParametersNotNull(s,“s”)
,因此比它的JAVA对应项更重。如果您不想要这个自动生成的方法,那么您可以在编译过程中禁用null检查


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

Kotlin只是稍微需要堆栈(Int-object-params i.o.Int-params)。除了适用于此处的tailrec解决方案外,您还可以通过xor 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)
}

当我测试这些时,我发现Java版本适用于29500左右的数组大小,但Kotlin版本将在18500左右停止。这是一个显著的区别,但不是很大的区别。如果您需要在大型阵列上使用它,唯一好的解决方案是使用
tailrec
,或者避免递归;可用的堆栈大小在运行之间、JVM和设置之间以及取决于方法及其参数而有所不同。但如果你纯粹出于好奇(这是一个很好的理由!),那么我不确定。您可能需要查看字节码。@user207421每个方法调用都有自己的堆栈框架,包括
Intrinsics.checkParametersNotFull(…)
。显然,每个这样的堆栈帧都需要一定量的内存(对于
LocalVariableTable
和操作数堆栈等等)。。
@Test
fun testKotlinImplementation() {
    val chars = CharArray(20000)
    Example().reverseString(chars)
}
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);
}
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)
}
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)
}