I want to know why this function works in java and also in kotlin with tailrec
but not in kotlin without tailrec
?
The short answer is because your Kotlin method is "heavier" than the JAVA one. At every call it calls another method that "provokes" StackOverflowError
. So, see a more detailed explanation below.
Java bytecode equivalents for reverseString()
I checked the byte code for your methods in Kotlin and JAVA correspondingly:
Kotlin method bytecode in JAVA
...
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
...
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:
Intrinsics.checkParameterIsNotNull(s, "s")
is invoked for each helper()
in the Kotlin version.
- Left and right indices in JAVA method get incremented, while in Kotlin new indices are created for each recursive call.
So, let's test how Intrinsics.checkParameterIsNotNull(s, "s")
alone affects the behavior.
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
. However, after I added Intrinsics.checkParameterIsNotNull(s, "s")
to the JAVA method it failed as well:
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. If you don't want this auto-generated method then you can disable null checks during compilation as answered here
However, since you understand what benefit tailrec
brings (converts your recursive call into an iterative one) you should use that one.