Hello.
I was solving this problem in LeetCode which you should reverse a string. I used the Kotlin language to solve it and wrote a recursive function:
class Solution {
fun rec(s: CharArray, i: Int, j: Int) {
if (i >= j) return
val t = s[j]
s[j] = s[i]
s[i] = t
rec(s, i + 1, j - 1)
}
fun reverseString(s: CharArray): Unit {
rec(s, 0, s.lastIndex)
}
}
which works for the small test cases. But it fails to pass one of the test cases which is an array of ~53k elements. Then I looked at solutions and saw a Java solution which is identical to mine
class Solution {
public void rec(char[] s, int i, int j) {
if (i >= j) return;
char tmp = s[i];
s[i] = s[j];
s[j] = tmp;
rec(s, i + 1, j - 1);
}
public void reverseString(char[] s) {
rec(s, 0, s.length - 1);
}
}
This does pass all tests and is an acceptable answer to the solution.
Can someone explain why the Java solution passes but the Koltin one doesn’t?
P.S I know that using tailrec does solve the problem. Also, have tried to read the generated bytecode but I am not good at understanding it.
EDIT Forgot to mention that the kotlin solution fails by throwing Stackoverflow exception.
This is not a reasonable way to reverse a string. Stack space is limited in Java and Kotlin, and shouldn’t be wasted.
With a 50K character input, your call stack will get up to 25K calls deep. With the default 1MB stack size, that allows less than 40 bytes for each stack frame. It may fit in Java, but just. The Kotlin environment probably just has a few more methods on the stack when yours is called.
A good rule of thumb is that a practical recursive algorithm should use no more than O(log N) stack space for inputs of size N.
I know there are better ways to reverse a string. It just wanted to know why it works in Java and not in Kotlin just out of curiosity Thanks for explanation.
Java does have automatically tail call optimization(TCO) while Kotlin doesn’t, you’ve to mark your function with tailrec explicitly to enable the optimization.
That’s interesting. Can you prove somehow that Java optimizes those calls? When I made a test then I got java.lang.StackOverflowError in both Java & Kotlin unless I used tailrec modifier.
It’s important to note that this isn’t a bug in the JVM. It’s an optimization that can be implemented to help functional programmers who use recursion, which is much more common and normal in those languages. I recently spoke to Brian Goetz at Oracle about this optimization, and he said that it’s on a list of things to be added to the JVM, but it’s just not a high-priority item. For now, it’s best to make this optimization yourself, if you can, by avoiding deeply recursive functions when coding a functional language on the JVM.
I for one would not like an automatic tailrec optimization, because the optimization can be easily broken by even slight changes to the functions implementation. Suddenly the optimization would not be applied anymore at runtime. I like that Kotlin yields a compilation error when the function is declared tailrec without correct implementation.
The keyword serves as a documentation for my co-developers, too. They cannot accidentally break my tail recursion.
@aminmsvi Did you find real cause of this problem, if yes, please share.
I’m also facing same issue. Same code in java working fine but in Kotlin throwing Stack Overflow.
See this code in link, java version works but Kotlin doesn’t.
Hello, rec method can be called from Java, so Kotlin has to check whether is CharArray nullable. Because of that, it will drain stack faster than the Java version.
Generated code
public final class Solution {
public final void rec(@NotNull char[] s, int i, int j) {
Intrinsics.checkParameterIsNotNull(s, "s");
if (i < j) {
char t = s[j];
s[j] = s[i];
s[i] = t;
this.rec(s, i + 1, j - 1);
}
}
public final void reverseString(@NotNull char[] s) {
Intrinsics.checkParameterIsNotNull(s, "s");
this.rec(s, 0, ArraysKt.getLastIndex(s));
}
}
Making rec method private should fix the problem as it won’t have to check nullability of the array.
I don’t mean that one, I mean that it may create both public final void rec(...) and public final Unit rec(...) where the unit one calls the void one (so Java code can safely override – I know it is a final method, the compiler is not that smart yet). Normally this indirection is insignificant and can be optimized away, but it does interfere with your stack.