Ereditarietà e ricorsione


86

Supponiamo di avere le seguenti classi:

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

}

Ora chiamiamo recursivein classe A:

public class Demo {

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

}

L'output è, come previsto, un conto alla rovescia da 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)

Veniamo alla parte confusa. Ora chiamiamo recursivein classe B.

Previsto :

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)

Attuale :

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

Come avviene questo? So che questo è un esempio inventato, ma mi meraviglia.

Domanda precedente con un caso d'uso concreto .


1
La parola chiave di cui hai bisogno sono tipi statici e dinamici! dovresti cercarlo e leggere un po 'al riguardo.
ParkerHalo


1
Per ottenere i risultati desiderati, estrai il metodo ricorsivo in un nuovo metodo privato.
Onots

1
@Onots Penso che rendere statici i metodi ricorsivi sarebbe più pulito.
risveglio

1
È semplice se noti che la chiamata ricorsiva in Aviene effettivamente inviata dinamicamente al recursivemetodo dell'oggetto corrente. Se stai lavorando con un Aoggetto, la chiamata ti porta a A.recursive(), e con un Boggetto, a B.recursive(). Ma B.recursive()chiama sempre A.recursive(). Quindi, se inizi un Boggetto, questo cambia avanti e indietro.
LIProf

Risposte:


76

Questo è previsto. Questo è ciò che accade per un'istanza di 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
    }

}

Pertanto, le chiamate si alternano tra Ae B.

Questo non accade nel caso di un'istanza di Aperché il metodo sovrascritto non verrà chiamato.


29

Perché recursive(i - 1);in si Ariferisce a this.recursive(i - 1);quale è B#recursivenel secondo caso. Quindi, supere thisverrà chiamato in funzione ricorsiva in alternativa .

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

nel A

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

27

Le altre risposte hanno tutte spiegato il punto essenziale, che una volta che un metodo di istanza viene sovrascritto, rimane sovrascritto e non è possibile ripristinarlo se non attraverso super. B.recursive()invoca A.recursive(). A.recursive()quindi invoca recursive(), che si risolve nell'override in B. E facciamo ping pong avanti e indietro fino alla fine dell'universo o StackOverflowError, a seconda di cosa si verifica per prima.

Sarebbe bello se si potesse scrivere this.recursive(i-1)in Aper ottenere la propria implementazione, ma che sarebbe probabilmente rompere le cose e avere altre conseguenze nefaste, così this.recursive(i-1)in Ainvoca B.recursive()e così via.

C'è un modo per ottenere il comportamento previsto, ma richiede lungimiranza. In altre parole, devi sapere in anticipo che vuoi che super.recursive()un sottotipo di Arimanga intrappolato, per così dire, Anell'implementazione. È fatto così:

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

Poiché A.recursive()invoca doRecursive()e doRecursive()non può mai essere sovrascritto, Aè certo che richiama la propria logica.


Mi chiedo, perché la chiamata doRecursive()interna recursive()dall'oggetto Bfunziona. Come ha scritto TAsk nella sua risposta, una chiamata di funzione funziona in modo simile this.doRecursive()e Object B( this) non ha metodo doRecursive()perché è nella classe Adefinita come privatee non protectede quindi non verrà ereditata, giusto?
Timo Denk

1
L'oggetto Bnon può chiamare doRecursive()affatto. doRecursive()è privatesì. Ma quando Bchiama super.recursive(), richiama l'implementazione di recursive()in A, che ha accesso a doRecursive().
Erick G. Hagstrom

2
Questo è esattamente l'approccio consigliato da Bloch in Effective Java se è assolutamente necessario consentire l'ereditarietà. Item 17: "Se ritieni di dover consentire l'ereditarietà da [una classe concreta che non implementa un'interfaccia standard], un approccio ragionevole è quello di garantire che la classe non invoca mai nessuno dei suoi metodi sovrascrivibili e di documentare il fatto."
Joe

16

super.recursive(i + 1);in class Bchiama esplicitamente il metodo della super classe, quindi recursiveof Aviene chiamato una volta.

Quindi, recursive(i - 1);in classe A chiamerebbe il recursivemetodo in classe Bche sovrascrive recursivedi classe A, poiché viene eseguito su un'istanza di classe B.

Poi B's recursivechiamerebbero A' s recursivein modo esplicito, e così via.


16

In realtà non può andare in nessun altro modo.

Quando chiami B.recursive(10);, stampa B.recursive(10)quindi chiama l'implementazione di questo metodo Acon i+1.

Quindi chiami A.recursive(11), che stampa A.recursive(11)che chiama il recursive(i-1);metodo sull'istanza corrente che è Bcon il parametro di input i-1, quindi chiama B.recursive(10), che quindi chiama la super implementazione con i+1cui is 11, che quindi chiama ricorsivamente l'istanza corrente ricorsiva con i-1cui è 10, e tu ottieni il ciclo che vedi qui.

Questo è tutto perché se chiami il metodo dell'istanza nella superclasse, chiamerai comunque l'implementazione dell'istanza su cui la stai chiamando.

Immagina questo,

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

Otterrai "BARK" invece di un errore di compilazione come "il metodo astratto non può essere chiamato su questa istanza" o un errore di runtime AbstractMethodErroro anche pure virtual method callqualcosa del genere. Quindi questo è tutto per supportare il polimorfismo .


14

Quando Bil recursivemetodo di un'istanza chiama l' superimplementazione della classe, l'istanza su cui si agisce è ancora di B. Pertanto, quando l'implementazione della super classe chiama recursivesenza ulteriore qualificazione, questa è l'implementazione della sottoclasse . Il risultato è il ciclo infinito che stai vedendo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.