Mam dwa prawie identyczny kod w java i kotlin
Jawa:
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);
}
Kotlin:
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)
}
Kod Java przechodzi test z ogromnym wejściem, ale kod kotlin powoduje, StackOverFlowError
że chyba, że dodałem tailrec
słowo kluczowe przed helper
funkcją w kotlin.
Chcę wiedzieć, dlaczego ta funkcja działa w Javie, a także w Kolinie, tailrec
ale bez Kotlina bez tailrec
?
PS:
wiem co tailrec
robię
tailrec
lub uniknięcie rekurencji; dostępny rozmiar stosu różni się między przebiegami, między maszynami JVM i konfiguracjami oraz w zależności od metody i jej parametrów. Ale jeśli pytasz z czystej ciekawości (doskonały powód!), To nie jestem pewien. Prawdopodobnie będziesz musiał spojrzeć na kod bajtowy.Odpowiedzi:
Krótka odpowiedź brzmi, ponieważ twoja metoda Kotlina jest „cięższa” niż metoda JAVA . Przy każdym wywołaniu wywołuje inną metodę, która „prowokuje”
StackOverflowError
. Zobacz więc bardziej szczegółowe wyjaśnienie poniżej.Ekwiwalenty kodu bajtowego Java dla
reverseString()
Sprawdziłem kod bajtu dla twoich metod odpowiednio w Kotlin i JAVA :
Kod bajtowy metody Kotlin w JAVA
Kod bajtowy metody JAVA w JAVA
Istnieją więc dwie główne różnice:
Intrinsics.checkParameterIsNotNull(s, "s")
jest wywoływany dla każdegohelper()
w wersji Kotlin .Przetestujmy, jak
Intrinsics.checkParameterIsNotNull(s, "s")
sam wpływa na zachowanie.Przetestuj obie implementacje
Stworzyłem prosty test dla obu przypadków:
I
W przypadku JAVA test powiódł się bez problemów, podczas gdy w przypadku Kotlin nie powiódł się z powodu błędu
StackOverflowError
. Jednakże, po dodaniuIntrinsics.checkParameterIsNotNull(s, "s")
do JAVA sposób nie udało się także:Wniosek
Twoja metoda Kotlin ma mniejszą głębokość rekurencji, ponieważ wywołuje ją
Intrinsics.checkParameterIsNotNull(s, "s")
na każdym kroku, a zatem jest cięższa niż jej odpowiednik JAVA . Jeśli nie chcesz tej automatycznie generowanej metody, możesz wyłączyć sprawdzanie wartości NULL podczas kompilacji, zgodnie z odpowiedzią tutajPonieważ jednak rozumiesz, co
tailrec
przynosi korzyść (zamienia rekursywne wywołanie w iteracyjne), powinieneś go użyć.źródło
Intrinsics.checkParameterIsNotNull(...)
. Oczywiście każda taka rama stosu wymaga pewnej ilości pamięci (dlaLocalVariableTable
stosu i operandu itd.).Kotlin jest tylko trochę bardziej głodny na stosach (Int object params io int params). Oprócz rozwiązania tailrec, które tu pasuje, możesz wyeliminować zmienną lokalną
temp
poprzez xor-ing:Nie do końca wiadomo, czy to działa, aby usunąć zmienną lokalną.
Również eliminacja j może zrobić:
źródło