Quiero saber por qué esta función funciona en Java y también en Kotlin con tailrec
pero no en Kotlin sin tailrec
.
La respuesta corta es porque su método Kotlin es "más pesado" que el de JAVA . En cada llamada se llama a otro método que "provoca" StackOverflowError
. Entonces, vea una explicación más detallada a continuación.
Java bytecode equivalentes para reverseString()
Verifiqué el código de byte para sus métodos en Kotlin y JAVA correspondientemente:
Código de bytes del método Kotlin en 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);
}
}
...
Código de bytes del método JAVA en 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);
}
}
...
Entonces, hay 2 diferencias principales:
Intrinsics.checkParameterIsNotNull(s, "s")
se invoca para cada uno helper()
en la versión de Kotlin .
- Los índices izquierdo y derecho en el método JAVA se incrementan, mientras que en Kotlin se crean nuevos índices para cada llamada recursiva.
Entonces, probemos cómo Intrinsics.checkParameterIsNotNull(s, "s")
solo afecta el comportamiento.
Probar ambas implementaciones
He creado una prueba simple para ambos casos:
@Test
public void testJavaImplementation() {
char[] chars = new char[20000];
new Example().reverseString(chars);
}
Y
@Test
fun testKotlinImplementation() {
val chars = CharArray(20000)
Example().reverseString(chars)
}
Para JAVA, la prueba tuvo éxito sin problemas, mientras que para Kotlin falló miserablemente debido a a StackOverflowError
. Sin embargo, después de agregar Intrinsics.checkParameterIsNotNull(s, "s")
al método JAVA , también falló:
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);
}
Conclusión
Su método Kotlin tiene una profundidad de recursión menor, ya que invoca Intrinsics.checkParameterIsNotNull(s, "s")
en cada paso y, por lo tanto, es más pesado que su contraparte JAVA . Si no desea este método generado automáticamente, puede deshabilitar las comprobaciones nulas durante la compilación como se responde aquí
Sin embargo, dado que comprende qué beneficio tailrec
trae (convierte su llamada recursiva en una iterativa), debe usar esa.
tailrec
o evitar la recurrencia ; el tamaño de la pila disponible varía entre ejecuciones, entre JVM y configuraciones, y dependiendo del método y sus parámetros. Pero si lo preguntas por pura curiosidad (¡una razón perfectamente buena!), Entonces no estoy seguro. Probablemente necesite mirar el código de bytes.