Eu quero saber por que essa função funciona em java e também no kotlin com tailrec
mas não no kotlin sem tailrec
?
A resposta curta é porque seu método Kotlin é "mais pesado" que o método JAVA . A cada chamada, chama outro método que "provoca" StackOverflowError
. Então, veja uma explicação mais detalhada abaixo.
Equivalentes de bytecode Java para reverseString()
Eu verifiquei o código de bytes para seus métodos no Kotlin e JAVA correspondentemente:
Método de Kotlin bytecode 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);
}
}
...
Bytecode do método JAVA 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, existem 2 diferenças principais:
Intrinsics.checkParameterIsNotNull(s, "s")
é chamado para cada um helper()
na versão Kotlin .
- Os índices esquerdo e direito no método JAVA são incrementados, enquanto no Kotlin novos índices são criados para cada chamada recursiva.
Então, vamos testar como Intrinsics.checkParameterIsNotNull(s, "s")
sozinho afeta o comportamento.
Teste as duas implementações
Eu criei um teste simples para os dois 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 foi bem-sucedido sem problemas, enquanto para Kotlin falhou miseravelmente devido a um StackOverflowError
. No entanto, depois que adicionei Intrinsics.checkParameterIsNotNull(s, "s")
ao método JAVA , ele também falhou:
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 método Kotlin possui uma profundidade de recursão menor, pois invoca Intrinsics.checkParameterIsNotNull(s, "s")
a cada passo e, portanto, é mais pesado que o equivalente em JAVA . Se você não quiser esse método gerado automaticamente, poderá desativar as verificações nulas durante a compilação, conforme respondidas aqui
No entanto, como você entende o que o benefício tailrec
traz (converte sua chamada recursiva em iterativa), você deve usá-la.
tailrec
ou evitar a recursão; o tamanho da pilha disponível varia entre execuções, entre JVMs e configurações e dependendo do método e de seus parâmetros. Mas se você está perguntando por pura curiosidade (uma razão perfeitamente boa!), Não tenho certeza. Você provavelmente precisaria olhar o código de bytes.