Я хочу знать, почему эта функция работает в Java, а также в Kotlin с, tailrec
но не в Kotlin без tailrec
?
Короткий ответ - потому что ваш метод Котлина "тяжелее", чем метод JAVA . При каждом вызове он вызывает другой метод, который «провоцирует» StackOverflowError
. Итак, смотрите более подробное объяснение ниже.
Java байт-код эквиваленты для reverseString()
Я проверил байт-код для ваших методов в Kotlin и 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()
в версии Kotlin .
- Левый и правый индексы в методе JAVA увеличиваются, тогда как в Kotlin новые индексы создаются для каждого рекурсивного вызова.
Итак, давайте проверим, как 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 тест прошел без проблем, в то время как для Kotlin он провалился из-за 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);
}
Вывод
Ваш метод Kotlin имеет меньшую глубину рекурсии, так как он вызывается Intrinsics.checkParameterIsNotNull(s, "s")
на каждом шаге и, следовательно, тяжелее своего аналога JAVA . Если вам не нужен этот автоматически сгенерированный метод, вы можете отключить проверку на ноль во время компиляции, как здесь ответили
Однако, поскольку вы понимаете, какую пользу tailrec
приносит (преобразует ваш рекурсивный вызов в итеративный), вы должны использовать его.
tailrec
или избегать рекурсии; Доступный размер стека варьируется между запусками, между JVM и настройками, а также в зависимости от метода и его параметров. Но если вы спрашиваете из чистого любопытства (совершенно веская причина!), То я не уверен. Вам, вероятно, нужно посмотреть на байт-код.