Я хочу знати, чому ця функція працює в java, а також в kotlin з, tailrec
але не в kotlin без tailrec
?
Коротка відповідь полягає в тому, що ваш метод Котліна "важчий", ніж метод JAVA . На кожен дзвінок він називає інший метод, який "провокує" StackOverflowError
. Отже, докладніше пояснення див. Нижче.
Еквіваленти байт-коду Java для reverseString()
Я перевірив байт-код на ваші методи у Котліні та JAVA відповідно:
Байт-код методу Котліна в 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 в 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 основні відмінності:
Intrinsics.checkParameterIsNotNull(s, "s")
викликається для кожного helper()
у версії Котліна .
- Ліві та праві індекси методу JAVA збільшуються, тоді як у Котліні створюються нові індекси для кожного рекурсивного виклику.
Отже, давайте перевіримо, як 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 тест вдався без проблем, тоді як для Котліна він зазнав невдач через a StackOverflowError
. Однак після того, як я додав Intrinsics.checkParameterIsNotNull(s, "s")
до методу JAVA, він також не вдався:
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);
}
Висновок
Ваш метод Котліна має меншу глибину рекурсії, оскільки він викликає Intrinsics.checkParameterIsNotNull(s, "s")
на кожному кроці і, таким чином, важчий, ніж його аналог JAVA . Якщо ви не хочете цього автоматично створеного методу, ви можете відключити нульові перевірки під час компіляції, як тут відповіли
Однак, оскільки ви розумієте, що tailrec
приносить користь (перетворює ваш рекурсивний дзвінок в ітеративний), вам слід скористатися цією.
tailrec
або уникнення рекурсії; доступний розмір стека залежить від прогонів, між JVM і налаштуваннями, і залежно від методу та його параметрів. Але якщо ви просите з чистої цікавості (цілком поважна причина!), То я не впевнений. Вам, мабуть, доведеться подивитися на байт-код.