Vererbung und Rekursion

86

Angenommen, wir haben die folgenden Klassen:

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

}

Rufen wir jetzt recursiveKlasse A an:

public class Demo {

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

}

Die Ausgabe wird erwartungsgemäß von 10 heruntergezählt.

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)

Kommen wir zum verwirrenden Teil. Jetzt rufen wir recursiveKlasse B an.

Erwartet :

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)

Tatsächlich :

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

Wie kommt es dazu? Ich weiß, dass dies ein erfundenes Beispiel ist, aber ich wundere mich darüber.

Ältere Frage mit einem konkreten Anwendungsfall .

Björn Raupach
quelle
1
Das Schlüsselwort, das Sie benötigen, sind statische und dynamische Typen! Sie sollten danach suchen und ein wenig darüber lesen.
ParkerHalo
1
Um die gewünschten Ergebnisse zu erzielen, extrahieren Sie die rekursive Methode in eine neue private Methode.
Onots
1
@Onots Ich denke, die rekursiven Methoden statisch zu machen wäre sauberer.
Ricksmt
1
Es ist einfach, wenn Sie feststellen, dass der rekursive Aufruf Atatsächlich dynamisch an die recursiveMethode des aktuellen Objekts gesendet wird. Wenn Sie mit einem AObjekt arbeiten, führt Sie der Aufruf zu A.recursive()und mit einem BObjekt zu B.recursive(). Ruft aber B.recursive()immer an A.recursive(). Wenn Sie also ein BObjekt starten , wechselt es hin und her.
LIProf

Antworten:

76

Dies wird erwartet. Dies ist, was für eine Instanz von passiert 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
    }

}

Daher wechseln die Anrufe zwischen Aund B.

Dies ist bei einer Instanz von nicht der Fall, Ada die Überschreibungsmethode nicht aufgerufen wird.

Tunaki
quelle
29

Denn recursive(i - 1);in Abezieht sich auf this.recursive(i - 1);was B#recursiveim zweiten Fall ist. Also superund thiswird alternativ in rekursiver Funktion aufgerufen .

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

im A

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

Die anderen Antworten haben alle den wesentlichen Punkt erklärt: Sobald eine Instanzmethode überschrieben wird, bleibt sie überschrieben und kann nur durch zurückgegeben werden super. B.recursive()ruft auf A.recursive(). A.recursive()ruft dann auf recursive(), was zum Überschreiben in auflöst B. Und wir ping pong hin und her bis zum Ende des Universums oder a StackOverflowError, je nachdem, was zuerst eintritt.

Es wäre schön, wenn man schreiben könnte this.recursive(i-1)in Aseine eigene Implementierung, aber das würde wahrscheinlich brechen Dinge und hat andere unangenehme Folgen, so this.recursive(i-1)in Aaufrufen B.recursive()und so weiter.

Es gibt einen Weg, um das erwartete Verhalten zu erreichen, aber es erfordert Voraussicht. Mit anderen Worten, Sie müssen im Voraus wissen, dass super.recursive()ein Subtyp von Asozusagen in der AImplementierung eingeschlossen werden soll. Es wird so gemacht:

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

Da A.recursive()aufgerufen wird doRecursive()und doRecursive()niemals überschrieben werden kann, Awird sichergestellt, dass es seine eigene Logik aufruft.

Erick G. Hagstrom
quelle
Ich frage mich, warum es funktioniert, vom Objekt aus nach doRecursive()innen zu rufen . Wie TAsk in seiner Antwort schrieb, funktioniert ein Funktionsaufruf wie folgt und Object ( ) hat keine Methode, da es in der Klasse als und nicht definiert ist und daher nicht vererbt wird, oder? recursive()Bthis.doRecursive()BthisdoRecursive()Aprivateprotected
Timo Denk
1
Objekt Bkann überhaupt nicht aufrufen doRecursive(). doRecursive()ist privateja. Bei BAufrufen super.recursive()ruft dies jedoch die Implementierung von recursive()in auf A, auf das zugegriffen werden kann doRecursive().
Erick G. Hagstrom
2
Dies ist genau der Ansatz, den Bloch in Effective Java empfiehlt, wenn Sie die Vererbung unbedingt zulassen müssen. Punkt 17: "Wenn Sie der Meinung sind, dass Sie die Vererbung von [einer konkreten Klasse, die keine Standardschnittstelle implementiert] zulassen müssen, besteht ein vernünftiger Ansatz darin, sicherzustellen, dass die Klasse niemals eine ihrer überschreibbaren Methoden aufruft, und die Tatsache zu dokumentieren."
Joe
16

super.recursive(i + 1);in class Bruft die Methode der Superklasse explizit auf, also recursivevonA einmal aufgerufen wird.

Dann recursive(i - 1);A in der Klasse würde der Aufruf recursiveMethode in der Klasse , Bdie Vorrang vor recursiveder Klasse A, da sie auf eine Instanz der Klasse ausgeführt wirdB .

Dann B‚s recursivenennen würde A‘ s recursiveausdrücklich, und so weiter.

Eran
quelle
16

Das kann eigentlich nicht anders gehen.

Wenn Sie aufrufen B.recursive(10);, wird gedruckt B.recursive(10)und die Implementierung dieser Methode Amit aufgerufen i+1.

Sie rufen also auf A.recursive(11), was druckt, A.recursive(11)was die recursive(i-1);Methode für die aktuelle Instanz Bmit dem Eingabeparameter i-1aufruft B.recursive(10), und ruft auf , was dann die Super-Implementierung aufruft , mit i+1der is ist 11, und die dann rekursiv die aktuelle Instanz aufruft, mit i-1der is ist10 , und Sie werden Holen Sie sich die Schleife, die Sie hier sehen.

Dies liegt daran, dass Sie beim Aufrufen der Methode der Instanz in der Oberklasse immer noch die Implementierung der Instanz aufrufen, auf der Sie sie aufrufen.

Stell dir das vor,

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

Sie erhalten "BARK" anstelle eines Kompilierungsfehlers wie "Die abstrakte Methode kann in dieser Instanz nicht aufgerufen werden" oder eines Laufzeitfehlers AbstractMethodErroroder sogar pure virtual method calloder so ähnlich. Das alles soll also den Polymorphismus unterstützen .

EpicPandaForce
quelle
14

Wenn Bdie recursiveMethode einer Instanz die superKlassenimplementierung aufruft , ist die Instanz, auf die reagiert wird, immer noch von B. Wenn die Implementierung der Superklasse recursiveohne weitere Qualifikation aufgerufen wird , ist dies die Implementierung der Unterklasse . Das Ergebnis ist die Endlosschleife, die Sie sehen.

Jonrsharpe
quelle