Héritage et récursivité

86

Supposons que nous ayons les classes suivantes:

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

}

Maintenant, appelons recursiveen classe A:

public class Demo {

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

}

La sortie est, comme prévu, un compte à rebours à partir de 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)

Passons à la partie déroutante. Maintenant, nous appelons recursiveen classe B.

Attendu :

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)

Réel :

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

Comment cela peut-il arriver? Je sais que c'est un exemple inventé, mais cela me fait me demander.

Question plus ancienne avec un cas d'utilisation concret .

raupach
la source
1
Le mot clé dont vous avez besoin est des types statiques et dynamiques! vous devriez chercher cela et lire un peu à ce sujet.
ParkerHalo
1
Pour obtenir les résultats souhaités, extrayez la méthode récursive vers une nouvelle méthode privée.
Onots
1
@Onots Je pense que rendre les méthodes récursives statiques serait plus propre.
ricksmt
1
C'est simple si vous remarquez que l'appel récursif Aest en fait distribué dynamiquement à la recursiveméthode de l'objet courant. Si vous travaillez avec un Aobjet, l'appel vous amène à A.recursive(), et avec un Bobjet, à B.recursive(). Mais B.recursive()appelle toujours A.recursive(). Donc, si vous démarrez un Bobjet, il bascule d'avant en arrière.
LIProf

Réponses:

76

Ceci est attendu. C'est ce qui se passe pour une instance de 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
    }

}

En tant que tels, les appels alternent entre Aet B.

Cela ne se produit pas dans le cas d'une instance de Acar la méthode surchargée ne sera pas appelée.

Tunaki
la source
29

Parce que recursive(i - 1);dans Afait référence à this.recursive(i - 1);ce qui est B#recursivedans le second cas. Donc, superet thissera appelé en fonction récursive alternativement .

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

dans A

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

Les autres réponses ont toutes expliqué le point essentiel, qu'une fois qu'une méthode d'instance est surchargée, elle reste surchargée et il n'y a pas de retour, sauf à travers super. B.recursive()invoque A.recursive(). A.recursive()puis invoque recursive(), ce qui résout le remplacement dans B. Et nous ping-pong dans les deux sens jusqu'à la fin de l'univers ou un StackOverflowError, selon la première éventualité.

Ce serait bien si l' on pouvait écrire this.recursive(i-1)dans Apour obtenir sa propre mise en œuvre, mais ce serait probablement casser des choses et avoir d' autres conséquences fâcheuses, si this.recursive(i-1)en Ainvoque B.recursive()et ainsi de suite.

Il existe un moyen d'obtenir le comportement attendu, mais cela nécessite de la prévoyance. En d'autres termes, vous devez savoir à l'avance que vous voulez qu'un super.recursive()sous-type de Asoit piégé, pour ainsi dire, dans l' Aimplémentation. C'est fait comme ça:

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

Puisque A.recursive()invoque doRecursive()et doRecursive()ne peut jamais être remplacé, il Aest assuré qu'il appelle sa propre logique.

Erick G. Hagstrom
la source
Je me demande, pourquoi appeler doRecursive()à l' intérieur recursive()de l'objet Bfonctionne. Comme TAsk l'a écrit dans sa réponse, un appel de fonction fonctionne comme this.doRecursive()et Object B( this) n'a pas de méthode doRecursive()car il est dans la classe Adéfinie comme privateet non protectedet ne sera donc pas hérité, non?
Timo Denk
1
L'objet Bne peut pas du tout appeler doRecursive(). doRecursive()est private, oui. Mais lors des Bappels super.recursive(), cela invoque l'implémentation de recursive()in A, qui a accès à doRecursive().
Erick G.Hagstrom
2
C'est exactement l'approche que Bloch recommande dans Effective Java si vous devez absolument autoriser l'héritage. Point 17: "Si vous pensez que vous devez autoriser l'héritage d'une [classe concrète qui n'implémente pas une interface standard], une approche raisonnable est de s'assurer que la classe n'invoque jamais aucune de ses méthodes remplaçables et de documenter le fait."
Joe le
16

super.recursive(i + 1);in class Bappelle explicitement la méthode de la super classe, donc recursivede Aest appelée une fois.

Ensuite, recursive(i - 1);dans la classe A appellerait la recursiveméthode de la classe Bqui remplace la recursiveclasse A, puisqu'elle est exécutée sur une instance de la classe B.

Ensuite , Bl ' recursiveappelleraient Aest recursiveexplicitement, et ainsi de suite.

Eran
la source
16

Cela ne peut en fait pas aller autrement.

Lorsque vous appelez B.recursive(10);, il imprime B.recursive(10)puis appelle l'implémentation de cette méthode Aavec i+1.

Donc, vous appelez A.recursive(11), qui imprime A.recursive(11)qui appelle la recursive(i-1);méthode sur l'instance actuelle qui est Bavec le paramètre d'entrée i-1, donc il appelle B.recursive(10), qui appelle ensuite la super implémentation avec i+1laquelle est 11, qui appelle ensuite de manière récursive l'instance actuelle récursive avec i-1laquelle est 10, et vous obtenez la boucle que vous voyez ici.

Tout cela parce que si vous appelez la méthode de l'instance dans la superclasse, vous appellerez toujours l'implémentation de l'instance sur laquelle vous l'appelez.

Imagine ça,

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

Vous obtiendrez "BARK" au lieu d'une erreur de compilation telle que "la méthode abstraite ne peut pas être appelée sur cette instance" ou une erreur d'exécution AbstractMethodErrorou même pure virtual method callou quelque chose comme ça. Tout cela est donc pour soutenir le polymorphisme .

EpicPandaForce
la source
14

Lorsque Bla recursiveméthode d' une instance appelle l' superimplémentation de la classe, l'instance sur laquelle il agit est toujours de B. Par conséquent, lorsque l'implémentation de la super classe appelle recursivesans autre qualification, c'est l'implémentation de la sous-classe . Le résultat est la boucle sans fin que vous voyez.

Jonrsharpe
la source