У меня есть два почти идентичных кода в Java и Kotlin
Ява:
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)
}
Код Java проходит тест с огромным вводом, но код Kotlin вызывает, StackOverFlowError
если я не добавил tailrec
ключевое слово перед helper
функцией в Kotlin.
Я хочу знать, почему эта функция работает в Java, а также в Kolin с, tailrec
но не в Kotlin без tailrec
?
PS:
я знаю, что tailrec
делать
tailrec
или избегать рекурсии; Доступный размер стека варьируется между запусками, между JVM и настройками, а также в зависимости от метода и его параметров. Но если вы спрашиваете из чистого любопытства (совершенно веская причина!), То я не уверен. Вам, вероятно, нужно посмотреть на байт-код.Ответы:
Короткий ответ - потому что ваш метод Котлина "тяжелее", чем метод JAVA . При каждом вызове он вызывает другой метод, который «провоцирует»
StackOverflowError
. Итак, смотрите более подробное объяснение ниже.Java байт-код эквиваленты для
reverseString()
Я проверил байт-код для ваших методов в Kotlin и JAVA соответственно:
Байт-код метода Котлина в JAVA
Байт-код метода JAVA в JAVA
Итак, есть 2 основных отличия:
Intrinsics.checkParameterIsNotNull(s, "s")
вызывается для каждогоhelper()
в версии Kotlin .Итак, давайте проверим, как
Intrinsics.checkParameterIsNotNull(s, "s")
один влияет на поведение.Протестируйте обе реализации
Я создал простой тест для обоих случаев:
А также
Для JAVA тест прошел без проблем, в то время как для Kotlin он провалился из-за a
StackOverflowError
. Однако, после того, как я добавилIntrinsics.checkParameterIsNotNull(s, "s")
к методу JAVA, это также не удалось:Вывод
Ваш метод Kotlin имеет меньшую глубину рекурсии, так как он вызывается
Intrinsics.checkParameterIsNotNull(s, "s")
на каждом шаге и, следовательно, тяжелее своего аналога JAVA . Если вам не нужен этот автоматически сгенерированный метод, вы можете отключить проверку на ноль во время компиляции, как здесь ответилиОднако, поскольку вы понимаете, какую пользу
tailrec
приносит (преобразует ваш рекурсивный вызов в итеративный), вы должны использовать его.источник
Intrinsics.checkParameterIsNotNull(...)
. Очевидно, что каждый такой кадр стека требует определенного объема памяти (дляLocalVariableTable
стека и операнда и т. Д.) ..Kotlin просто чуть-чуть более требователен к стеку (параметры объекта Int и параметры типа int). Помимо подходящего здесь решения tailrec, вы можете исключить локальную переменную с
temp
помощью xor-ing:Не совсем уверен, работает ли это, чтобы удалить локальную переменную.
Также устранение j может сделать:
источник