Eu tenho dois código quase idêntico em java e Kotlin
Java:
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)
}
O código java passar no teste com uma enorme entrada, mas o código Kotlin causa a StackOverFlowError
menos que eu adicionei tailrec
palavra-chave antes da helper
função em Kotlin.
Eu quero saber por que essa função funciona em java e também em Kolin com tailrec
mas não em Kotlin sem tailrec
?
PS: eu sei o que tailrec
fazer
Eu quero saber por que essa função funciona em java e também em Kotlin com
tailrec
mas não em Kotlin semtailrec
?
A resposta curta é porque o seu Kotlin método é "mais pesado" do que o JAVA um. Em cada chamada que chama outro método que "provoca" StackOverflowError
. Então, ver uma explicação mais detalhada abaixo.
equivalentes bytecode Java para reverseString()
Eu verifiquei o código de byte para os seus métodos em Kotlin e JAVA correspondentemente:
método Kotlin código de bytes em 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);
}
}
...
método Java código de bytes em 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);
}
}
...
Então, lá está 2 diferenças principais:
Intrinsics.checkParameterIsNotNull(s, "s")
é invocado para cadahelper()
no Kotlin versão.- Índices de esquerda e direita em JAVA método se incrementado, enquanto em Kotlin novos índices são criados para cada chamada recursiva.
Assim, o teste de deixar como Intrinsics.checkParameterIsNotNull(s, "s")
só afeta o comportamento.
Testar ambas as implementações
Eu criei um teste simples para ambos os casos:
@Test
public void testJavaImplementation() {
char[] chars = new char[20000];
new Example().reverseString(chars);
}
E
@Test
fun testKotlinImplementation() {
val chars = CharArray(20000)
Example().reverseString(chars)
}
Para JAVA o teste teve sucesso sem problemas enquanto para Kotlin falhou miseravelmente, devido a uma StackOverflowError
. No entanto, depois que eu adicionei Intrinsics.checkParameterIsNotNull(s, "s")
ao JAVA método que falhou, bem como:
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);
}
Conclusão
Seu Kotlin método tem uma profundidade de recursão menor, uma vez que invoca Intrinsics.checkParameterIsNotNull(s, "s")
a cada passo e, portanto, é mais pesado do que o seu JAVA homólogo. Se você não quiser este método gerado automaticamente, então você pode desabilitar a verificação de nulos durante a compilação como respondida aqui
No entanto, desde que você entender o que benefício tailrec
traz (converte a sua chamada recursiva em um um iterativo), você deve usar esse.