我在java和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)
}

java代码以巨大的输入通过了测试,但kotlin代码会导致StackOverFlowError,除非我在kotlin中的helper函数之前添加了tailrec关键字.

I want to know why this function works in java and also in kolin with tailrec but not in kotlin without tailrec?

P.S: i know what tailrec do

推荐答案

我想知道为什么这个函数在java中有效,在kotlin中有tailrec,但在kotlin中没有tailrec

简短的答案是因为您的Kotlin方法比JAVA方法"重".在每次调用时,它都会调用另一个"激发"StackOverflowError的方法.因此,请参阅下面更详细的说明.

Java bytecode equivalents for reverseString()

我在KotlinJAVA中相应地判断了你的方法的字节码:

Kotlin method bytecode 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);
    }
}
...

JAVA method bytecode 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);
    }
}
...

So, there're 2 main differences:

  1. Intrinsics.checkParameterIsNotNull(s, "s") is invoked for each helper() in the Kotlin version.
  2. JAVA方法中的左索引和右索引递增,而在Kotlin方法中, for each 递归调用创建新索引.

So, let's test how Intrinsics.checkParameterIsNotNull(s, "s") alone affects the behavior.

Test both implementations

我为这两种情况创建了一个简单的测试:

@Test
public void testJavaImplementation() {
    char[] chars = new char[20000];
    new Example().reverseString(chars);
}

@Test
fun testKotlinImplementation() {
    val chars = CharArray(20000)
    Example().reverseString(chars)
}

For JAVA the test succeeded without problems while for Kotlin it failed miserably due to a StackOverflowError. However, after I added Intrinsics.checkParameterIsNotNull(s, "s") to the JAVA method it failed as well:

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);
}

Conclusion

Kotlin方法的递归深度较小,因为它在每一步调用Intrinsics.checkParameterIsNotNull(s, "s"),因此比JAVA方法重.如果您不想使用这个自动生成的方法,那么可以在编译过程中禁用null判断,如下所示

然而,既然您了解tailrec带来的好处(将递归调用转换为迭代调用),您就应该使用它.

Kotlin相关问答推荐

升级使用jOOQ Gradle插件生成代码失败

房间数据库操作中的协程取消

collectAsState 未从存储库接收更改

列表在 android WebView 中没有正确迭代

在 kotlin 中使具体化字段可选

在 kotlin 原始字符串中转义三重引号

如何在 Android Jetpack Compose 中的画布上绘制一侧加厚的描边?

kotlin 单例异常是好是坏?

如何使用 Hilt 注入应用程序:ViewModel 中的上下文?

Android Kotlin StringRes 数量String

kotlin:扩展方法和空接收器

Jacoco在Gradle 7.0.2和Kotlin 1.5.10上失败

用于代码生成的ANTLR工具版本4.7.1与当前运行时版本4.5.3不匹配

什么是 Kotlin 等价于 Class<?>

Kotlin扩展函数与成员函数?

在 Kotlin 中声明 Byte 会出现编译时错误The integer literal does not conform to the expected type Byte

比较Kotlin的NaN

Java的Kotlin:字段是否可以为空?

从另一个列表创建一个列表

java.lang.NoClassDefFoundError:解析失败:Lkotlin/time/MonoClock