递归方法调用在kotlin中会导致StackOverflower错误,但在java中不会
我在java和kotlin中有两个几乎相同的代码 Java: Kotlin: java代码以巨大的输入通过了测试,但kotlin代码会导致递归方法调用在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
StackOverflowerError
,除非我在kotlin中的helper
函数之前添加了tailrec
关键字
我想知道为什么这个函数在java和kolin中使用tailrec
,但在kotlin中不使用tailrec
p.S:
我知道tailrec做什么
我想知道为什么这个函数在java和kotlin中使用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)
}
我在Kotlin和JAVA中相应地检查了方法的字节码:
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)
}