Warisan dan rekursi

86

Misalkan kita memiliki kelas-kelas berikut:

class A {

    void recursive(int i) {
        System.out.println("A.recursive(" + i + ")");
        if (i > 0) {
            recursive(i - 1);
        }
    }

}

class B extends A {

    void recursive(int i) {
        System.out.println("B.recursive(" + i + ")");
        super.recursive(i + 1);
    }

}

Sekarang mari panggil recursivedi kelas A:

public class Demo {

    public static void main(String[] args) {
        A a = new A();
        a.recursive(10);
    }

}

Outputnya, seperti yang diharapkan menghitung mundur dari 10.

A.recursive(10)
A.recursive(9)
A.recursive(8)
A.recursive(7)
A.recursive(6)
A.recursive(5)
A.recursive(4)
A.recursive(3)
A.recursive(2)
A.recursive(1)
A.recursive(0)

Mari kita ke bagian yang membingungkan. Sekarang kita sebut recursivedi kelas B.

Diharapkan :

B.recursive(10)
A.recursive(11)
A.recursive(10)
A.recursive(9)
A.recursive(8)
A.recursive(7)
A.recursive(6)
A.recursive(5)
A.recursive(4)
A.recursive(3)
A.recursive(2)
A.recursive(1)
A.recursive(0)

Sebenarnya :

B.recursive(10)
A.recursive(11)
B.recursive(10)
A.recursive(11)
B.recursive(10)
A.recursive(11)
B.recursive(10)
..infinite loop...

Bagaimana ini bisa terjadi? Saya tahu ini adalah contoh yang dibuat, tetapi itu membuat saya bertanya-tanya.

Pertanyaan lama dengan kasus penggunaan konkret .

raupach
sumber
1
Kata kunci yang Anda butuhkan adalah tipe statis dan dinamis! Anda harus mencari itu dan membaca sedikit tentang itu.
ParkerHalo
1
Untuk mendapatkan hasil yang Anda inginkan, ekstrak metode rekursif ke metode privat baru.
Onots
1
@Onots Saya pikir membuat metode rekursif statis akan lebih bersih.
ricksmt
1
Sederhana jika Anda memperhatikan bahwa panggilan rekursif Asebenarnya dikirim secara dinamis ke recursivemetode objek saat ini. Jika Anda bekerja dengan sebuah Aobjek, panggilan tersebut membawa Anda ke A.recursive(), dan dengan sebuah Bobjek, ke B.recursive(). Tapi B.recursive()selalu menelepon A.recursive(). Jadi, jika Anda memulai suatu Bobjek, itu beralih bolak-balik.
LIProf

Jawaban:

76

Ini diharapkan. Inilah yang terjadi untuk contoh B.

class A {

    void recursive(int i) { // <-- 3. this gets called
        System.out.println("A.recursive(" + i + ")");
        if (i > 0) {
            recursive(i - 1); // <-- 4. this calls the overriden "recursive" method in class B, going back to 1.
        }
    }

}

class B extends A {

    void recursive(int i) { // <-- 1. this gets called
        System.out.println("B.recursive(" + i + ")");
        super.recursive(i + 1); // <-- 2. this calls the "recursive" method of the parent class
    }

}

Karena itu, panggilan bergantian antara Adan B.

Ini tidak terjadi dalam kasus instance Akarena metode yang diganti tidak akan dipanggil.

Tunaki
sumber
29

Karena recursive(i - 1);di Amengacu pada this.recursive(i - 1);yang B#recursivedi kasus kedua. Jadi, superdan thisakan dipanggil dalam fungsi rekursif sebagai alternatif .

void recursive(int i) {
    System.out.println("B.recursive(" + i + ")");
    super.recursive(i + 1);//Method of A will be called
}

di A

void recursive(int i) {
    System.out.println("A.recursive(" + i + ")");
    if (i > 0) {
        this.recursive(i - 1);// call B#recursive
    }
}
CoderCroc
sumber
27

Jawaban lain semuanya telah menjelaskan poin penting, bahwa setelah metode instance diganti, ia tetap diganti dan tidak ada yang mendapatkannya kembali kecuali melalui super. B.recursive()memanggil A.recursive(). A.recursive()lalu memanggil recursive(), yang menyelesaikan penggantian di B. Dan kita ping pong bolak-balik sampai akhir alam semesta atau a StackOverflowError, mana yang lebih dulu.

Akan lebih baik jika salah satu bisa menulis this.recursive(i-1)di Auntuk mendapatkan implementasi sendiri, tapi itu mungkin akan melanggar hal-hal dan memiliki konsekuensi yang tidak menguntungkan lainnya, sehingga this.recursive(i-1)dalam Amemanggil B.recursive()dan sebagainya.

Ada cara untuk mendapatkan perilaku yang diharapkan, tetapi itu membutuhkan kejelian. Dengan kata lain, Anda harus tahu sebelumnya bahwa Anda ingin super.recursive()subtipe dari Aterjebak, sehingga untuk berbicara, dalam Aimplementasi. Ini dilakukan seperti:

class A {

    void recursive(int i) {
        doRecursive(i);
    }

    private void doRecursive(int i) {
        System.out.println("A.recursive(" + i + ")");
        if (i > 0) {
            doRecursive(i - 1);
        }
    }
}

class B extends A {

    void recursive(int i) {
        System.out.println("B.recursive(" + i + ")");
        super.recursive(i + 1);
    }
}

Sejak A.recursive()memanggil doRecursive()dan doRecursive()tidak pernah bisa diganti, Ayakinlah bahwa itu memanggil logikanya sendiri.

Erick G. Hagstrom
sumber
Saya bertanya-tanya, mengapa menelepon ke doRecursive()dalam recursive()dari objek itu Bberhasil. Seperti yang TAsk tulis dalam jawabannya, panggilan fungsi berfungsi seperti this.doRecursive()dan Object B( this) tidak memiliki metode doRecursive()karena berada di kelas yang Adidefinisikan sebagai privatedan tidak protecteddan karena itu tidak akan diwarisi, bukan?
Timo Denk
1
Objek Btidak bisa menelepon doRecursive()sama sekali. doRecursive()adalah privateya. Tapi saat Bpanggilan super.recursive(), itu memanggil implementasi recursive()dalam A, yang memiliki akses ke doRecursive().
Erick G. Hagstrom
2
Ini persis pendekatan yang direkomendasikan Bloch di Java Efektif jika Anda benar-benar harus mengizinkan warisan. Butir 17: "Jika Anda merasa harus mengizinkan pewarisan dari [kelas konkret yang tidak menerapkan antarmuka standar], satu pendekatan yang masuk akal adalah memastikan bahwa kelas tersebut tidak pernah memanggil metode yang dapat diganti dan mendokumentasikan fakta."
Joe
16

super.recursive(i + 1);di kelas Bmemanggil metode kelas super secara eksplisit, jadi recursivedari Adipanggil sekali.

Kemudian, recursive(i - 1);di kelas A akan memanggil recursivemetode di kelas Byang menggantikan recursivekelas A, karena itu dijalankan pada sebuah instance kelas B.

Kemudian B's recursiveakan memanggil A' s recursiveeksplisit, dan sebagainya.

Eran
sumber
16

Itu sebenarnya tidak bisa dilakukan dengan cara lain.

Ketika Anda memanggil B.recursive(10);, kemudian mencetak B.recursive(10)kemudian memanggil implementasi metode ini Adengan i+1.

Jadi Anda memanggil A.recursive(11), yang mencetak A.recursive(11)yang memanggil recursive(i-1);metode pada instance saat ini yang Bdengan parameter input i-1, sehingga memanggil B.recursive(10), yang kemudian memanggil implementasi super dengan i+1yang 11, yang kemudian secara rekursif memanggil instance saat ini yang rekursif i-1dengannya 10, dan Anda akan dapatkan lingkaran yang Anda lihat di sini.

Ini semua karena jika Anda memanggil metode instance di superclass, Anda masih akan memanggil implementasi instance yang Anda panggil.

Bayangkan ini,

 public abstract class Animal {

     public Animal() {
         makeSound();
     }

     public abstract void makeSound();         
 }

 public class Dog extends Animal {
     public Dog() {
         super(); //implicitly called
     }

     @Override
     public void makeSound() {
         System.out.println("BARK");
     }
 }

 public class Main {
     public static void main(String[] args) {
         Dog dog = new Dog();
     }
 }

Anda akan mendapatkan "BARK" sebagai ganti error kompilasi seperti "metode abstrak tidak dapat dipanggil pada instance ini" atau error runtime AbstractMethodErroratau bahkan pure virtual method callatau semacamnya. Jadi ini semua untuk mendukung polimorfisme .

EpicPandaForce
sumber
14

Saat metode Binstance recursivememanggil superimplementasi kelas, instance yang ditindaklanjuti masih dalam B. Oleh karena itu, ketika implementasi kelas super memanggil recursivetanpa kualifikasi lebih lanjut, itulah implementasi subkelas . Hasilnya adalah lingkaran tanpa akhir yang Anda lihat.

jonrsharpe
sumber