Özyinelemeli yöntem çağrısı neden kotlin'de StackOverFlowError ama java'da değil


14

Java ve kotlin'de neredeyse aynı kodum var

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 kodu büyük bir giriş ile testi geçmek ama kotlin fonksiyon önce anahtar kelime StackOverFlowErroreklemedik sürece kotlin kodu bir neden olur .tailrechelper

Neden bu fonksiyonun java ve tailreckolin ile çalıştığını bilmek istiyorum ama kotlin olmadan değil tailrecmi?

Not: ne tailrecyapacağımı biliyorum


1
Bunları test ettiğimde, Java sürümünün 29500'e kadar dizi boyutları için çalışacağını buldum, ancak Kotlin sürümü 18500 civarında duracaktı. Bu önemli bir fark, ama çok büyük değil. Büyük dizilerde çalışmak için buna ihtiyacınız varsa, tek iyi çözüm kullanmak tailrecveya özyinelemeden kaçınmaktır; kullanılabilir yığın boyutu, çalıştırmalar, JVM'ler ve kurulumlar arasında ve yönteme ve parametrelerine bağlı olarak değişir. Ama saf meraktan (mükemmel bir neden!) Soruyorsanız, emin değilim. Muhtemelen bayt koduna bakmanız gerekir.
gidds

Yanıtlar:


7

Neden bu fonksiyonun java ve kotlin ile çalıştığını , tailrecancak kotlin olmadan çalıştığını bilmek istiyorum tailrec?

Kısa cevap, Kotlin yönteminizin JAVA yönteminden "daha ağır" olmasıdır . Her çağrıda "kışkırtan" başka bir yöntem denir StackOverflowError. Aşağıda daha ayrıntılı bir açıklamaya bakın.

İçin Java bayt kodu eşdeğerleri reverseString()

Ben de senin yöntemleri için bayt kodu kontrol KOTLIN ve JAVA buna:

JAVA'da Kotlin yöntemi bayt kodu

...
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'da JAVA yöntemi bayt kodu

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

Yani, 2 ana fark var:

  1. Intrinsics.checkParameterIsNotNull(s, "s")Her biri için çağrılır helper()içinde Kotlin sürümü.
  2. JAVA yönteminde sol ve sağ indeksler artarken, Kotlin'de her özyinelemeli çağrı için yeni indeksler oluşturulur.

Şimdi Intrinsics.checkParameterIsNotNull(s, "s")davranışı nasıl tek başına etkilediğini test edelim .

Her iki uygulamayı da test edin

Her iki durum için de basit bir test oluşturdum:

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

Ve

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

İçin JAVA için ise sınav sorunsuz başarılı KOTLIN nedeniyle bir kaçýrýlmýþtýr StackOverflowError. Ben eklendi sonra ancak Intrinsics.checkParameterIsNotNull(s, "s")hiç JAVA yöntemiyle buna da başarısız oldu:

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

Sonuç

Kişisel Kotlin diline doladığı olarak yöntem daha küçük bir yineleme derinliğini sahip Intrinsics.checkParameterIsNotNull(s, "s")olan ağırdır böylece her adımda ve JAVA muadili. Bu otomatik oluşturulan yöntemi istemiyorsanız, derleme sırasında burada yanıtlandığı gibi boş denetimleri devre dışı bırakabilirsiniz

Ancak, hangi yararın tailrecgetirildiğini anladığınızdan (özyinelemeli çağrınızı yinelemeli bir çağrıya dönüştürür) bunu kullanmalısınız.


@ user207421 her yöntem çağırma dahil olmak üzere kendi yığın çerçevesi vardır Intrinsics.checkParameterIsNotNull(...). Açıkçası, bu tür her yığın çerçevesi belirli bir miktarda bellek gerektirir ( LocalVariableTableve işlenen yığını için vb.) ..
Anatolii

0

Kotlin sadece biraz daha açtır (Int nesnesi params io int params). Buraya uyan tailrec çözümünün yanı sıra, yerel değişkeni xoring yoluyla ortadan kaldırabilirsiniz 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)
}

Bunun yerel bir değişkeni kaldırmak için çalışıp çalışmadığından emin değilim.

Ayrıca j'nin ortadan kaldırılması şunları yapabilir:

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)
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.