Voglio sapere perché questa funzione funziona in java e anche in kotlin con tailrec
ma non in kotlin senza tailrec
?
La risposta breve è perché il tuo metodo Kotlin è "più pesante" di quello JAVA . Ad ogni chiamata chiama un altro metodo che "provoca" StackOverflowError
. Quindi, vedi una spiegazione più dettagliata di seguito.
Equivalenti bytecode Java per reverseString()
Ho controllato il codice byte per i tuoi metodi in Kotlin e JAVA di conseguenza:
Bytecode del metodo Kotlin in 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);
}
}
...
Bytecode del metodo JAVA in 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);
}
}
...
Quindi, ci sono 2 differenze principali:
Intrinsics.checkParameterIsNotNull(s, "s")
viene invocato per ciascuno helper()
nella versione di Kotlin .
- Gli indici sinistro e destro nel metodo JAVA vengono incrementati, mentre in Kotlin vengono creati nuovi indici per ogni chiamata ricorsiva.
Quindi, proviamo come Intrinsics.checkParameterIsNotNull(s, "s")
solo influenza il comportamento.
Testare entrambe le implementazioni
Ho creato un semplice test per entrambi i casi:
@Test
public void testJavaImplementation() {
char[] chars = new char[20000];
new Example().reverseString(chars);
}
E
@Test
fun testKotlinImplementation() {
val chars = CharArray(20000)
Example().reverseString(chars)
}
Per JAVA il test ha avuto successo senza problemi mentre per Kotlin ha fallito miseramente a causa di a StackOverflowError
. Tuttavia, dopo aver aggiunto Intrinsics.checkParameterIsNotNull(s, "s")
al metodo JAVA anche questo ha avuto esito negativo:
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);
}
Conclusione
Il tuo metodo Kotlin ha una profondità di ricorsione più piccola in quanto invoca Intrinsics.checkParameterIsNotNull(s, "s")
ad ogni passo ed è quindi più pesante della sua controparte JAVA . Se non si desidera questo metodo generato automaticamente, è possibile disabilitare i controlli null durante la compilazione, come indicato qui
Tuttavia, poiché capisci quale vantaggio tailrec
porta (converte la tua chiamata ricorsiva in una chiamata iterativa) dovresti usare quella.
tailrec
o evitare la ricorsione; la dimensione dello stack disponibile varia tra le esecuzioni, tra JVM e configurazioni e in base al metodo e ai suoi parametri. Ma se lo chiedi per pura curiosità (una ragione perfettamente valida!), Non ne sono sicuro. Probabilmente avresti bisogno di guardare il bytecode.