web-dev-qa-db-ja.com

再帰的なメソッド呼び出しにより、kotlinではStackOverFlowErrorが発生しますが、javaでは発生しません

Javaとkotlinにほぼ同じ2つのコードがあります

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コードは巨大な入力でテストに合格しますが、StackOverFlowErrorキーワードをtailrec関数の前に追加しない限り、kotlinコードはhelperを引き起こしますコトリンで。

この関数がJavaおよびtailrecを使用するkolinで機能するが、tailrecを使用しないkotlinでは機能しない理由を知りたいですか?

P.S:tailrecが何をするか知っています

14
hamid_c

この関数がJavakotlintailrecを使用してkotlintailrecを使用せずに機能する理由を知りたいですか?

短い答えは、KotlinメソッドがJavaの1つよりも「重い」ためです。呼び出しのたびに、それはStackOverflowErrorを「引き起こす」別のメソッドを呼び出します。したがって、以下の詳細な説明を参照してください。

Java reverseString()と同等のバイトコード

KotlinおよびJavaで対応するように、メソッドのバイトコードを確認しました。

JavaでのKotlinメソッドのバイトコード

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

したがって、2つの主な違いがあります。

  1. Intrinsics.checkParameterIsNotNull(s, "s")は、Kotlinバージョンのhelper()ごとに呼び出されます。
  2. Javaメソッドの左と右のインデックスはインクリメントされますが、Kotlinでは、再帰呼び出しごとに新しいインデックスが作成されます。

それでは、Intrinsics.checkParameterIsNotNull(s, "s")だけがどのように動作に影響するかをテストしてみましょう。

両方の実装をテスト

両方のケースで簡単なテストを作成しました。

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

そして

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

Javaの場合、テストは問題なく成功しましたが、Kotlinの場合、StackOverflowErrorが原因で惨めに失敗しました。ただし、Intrinsics.checkParameterIsNotNull(s, "s")を追加した後Javaメソッドにも同様に失敗しました:

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

結論

Kotlinメソッドは、すべてのステップでIntrinsics.checkParameterIsNotNull(s, "s")を呼び出すため、再帰の深さが小さく、Javaよりも重い)です。この自動生成されたメソッドが必要ない場合は、回答時にコンパイル中にnullチェックを無効にすることができます here

ただし、tailrecがもたらす利点(再帰呼び出しを反復呼び出しに変換する)を理解しているので、それを使用する必要があります。

7
Anatolii

Kotlinは、スタックが少しだけ空腹です(Intオブジェクトパラメータi.o. intパラメータ)。ここに当てはまるtailrecソリューションの他に、xor-ingによってローカル変数tempを削除できます:

fun helper(i: Int, j: Int, s: CharArray) {
    if (i >= j) {
        return
    }               // i: a          j: b
    s[j] ^= s[i]    //               j: a^b
    s[i] ^= s[j]    // i: a^a^b == b
    s[j] ^= s[i]    //               j: a^b^b == a
    helper(i + 1, j - 1, s)
}

これがローカル変数を削除するために機能するかどうかは完全にはわかりません。

また、jを削除すると、次のようになります。

fun reverseString(s: CharArray): Unit {
    helper(0, s)
}

fun helper(i: Int, s: CharArray) {
    if (i >= s.lastIndex - i) {
        return
    }
    val t = s[s.lastIndex - i]
    s[s.lastIndex - i] = s[i]
    s[i] = t
    helper(i + 1, s)
}
0
Joop Eggen