Herencia y recursividad

86

Supongamos que tenemos las siguientes clases:

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

}

Ahora llamemos recursivea la clase A:

public class Demo {

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

}

La salida es, como se esperaba, contando hacia atrás desde 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)

Vayamos a la parte confusa. Ahora llamamos recursivea la clase B.

Esperado :

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)

Actual :

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

¿Como sucedió esto? Sé que este es un ejemplo inventado, pero me hace preguntarme.

Pregunta anterior con un caso de uso concreto .

raupach
fuente
1
¡La palabra clave que necesita son tipos estáticos y dinámicos! deberías buscarlo y leer un poco al respecto.
ParkerHalo
1
Para obtener los resultados deseados, extraiga el método recursivo a un nuevo método privado.
Onots
1
@Onots Creo que hacer estáticos los métodos recursivos sería más limpio.
ricksmt
1
Es simple si observa que la llamada recursiva en Arealidad se envía dinámicamente al recursivemétodo del objeto actual. Si está trabajando con un Aobjeto, la llamada lo lleva a A.recursive(), y con un Bobjeto, a B.recursive(). Pero B.recursive()siempre llama A.recursive(). Entonces, si inicia un Bobjeto, cambia de un lado a otro.
LIProf

Respuestas:

76

Se esperaba esto. Esto es lo que sucede con una instancia 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
    }

}

Como tal, las llamadas se alternan entre Ay B.

Esto no sucede en el caso de una instancia de Aporque no se llamará al método anulado.

Atún
fuente
29

Porque recursive(i - 1);en se Arefiere a this.recursive(i - 1);cuál está B#recursiveen segundo caso. Entonces, supery thisse llamará alternativamente en función recursiva .

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

en A

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

Todas las otras respuestas han explicado el punto esencial, que una vez que se anula un método de instancia, permanece anulado y no se puede recuperar excepto a través super. B.recursive()invoca A.recursive(). A.recursive()luego invoca recursive(), que se resuelve en la invalidación B. Y hacemos ping pong de un lado a otro hasta el fin del universo o unStackOverflowError , lo que ocurra primero.

Sería bueno si se podría escribir this.recursive(i-1)en Aconseguir su propia implementación, pero que probablemente romper cosas y tienen otras consecuencias desafortunadas, por lo que this.recursive(i-1)en AinvocaB.recursive() y así sucesivamente.

Hay una forma de obtener el comportamiento esperado, pero requiere previsión. En otras palabras, debe saber de antemano que desea que super.recursive()un subtipo de Aquede atrapado, por así decirlo, en la Aimplementación. Se hace así:

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

Dado que A.recursive()invoca doRecursive()y doRecursive()nunca se puede invalidar, Ase asegura que está llamando a su propia lógica.

Erick G. Hagstrom
fuente
Me pregunto por qué funciona llamar al doRecursive()interior recursive()desde el objeto B. Como TAsk escribió en su respuesta, una llamada a función funciona como this.doRecursive()y Object B( this) no tiene un método doRecursive()porque está en la clase Adefinida como privatey no protectedy, por lo tanto, no se heredará, ¿verdad?
Timo Denk
1
El objeto Bno puede llamar doRecursive()en absoluto. doRecursive()es private, si. Pero cuando Bllama super.recursive(), eso invoca la implementación de recursive()in A, que tiene acceso a doRecursive().
Erick G. Hagstrom
2
Este es exactamente el enfoque que Bloch recomienda en Effective Java si es absolutamente necesario permitir la herencia. Ítem ​​17: "Si cree que debe permitir la herencia de [una clase concreta que no implementa una interfaz estándar], un enfoque razonable es asegurarse de que la clase nunca invoque ninguno de sus métodos reemplazables y documentar el hecho".
Joe
16

super.recursive(i + 1); en la clase B llama al método de la superclase explícita, de modo recursivedeA que se llama una vez.

Entonces, recursive(i - 1);en la clase A llamaría al recursivemétodo en la claseB que anula la recursiveclase A, ya que se ejecuta en una instancia de la clase.B .

Luego B's recursivellamarían A' s recursivede forma explícita, y así sucesivamente.

Eran
fuente
16

Eso en realidad no puede ser de otra manera.

Cuando llama B.recursive(10);, imprime y B.recursive(10)luego llama a la implementación de este método Aconi+1 .

Entonces llama A.recursive(11), que imprime A.recursive(11)cuál llama al recursive(i-1);método en la instancia actual que está Bcon el parámetro de entrada i-1, por lo que llama B.recursive(10), que luego llama a la súper implementación con la i+1cual es 11, que luego llama recursivamente a la instancia actual recursiva con la i-1cual es10 , y usted obtén el bucle que ves aquí.

Todo esto se debe a que si llama al método de la instancia en la superclase, aún llamará a la implementación de la instancia en la que la está llamando.

Imagina esto,

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

Obtendrá "BARK" en lugar de un error de compilación como "el método abstracto no se puede llamar en esta instancia" o un error de tiempo de ejecución AbstractMethodErroro incluso pure virtual method callo algo así. Así que todo esto es para apoyar el polimorfismo .

EpicPandaForce
fuente
14

Cuando Bel recursivemétodo de una instancia llama a la superimplementación de la clase, la instancia sobre la que se actúa sigue siendo de B. Por lo tanto, cuando la implementación de la superclase llama recursivesin más calificación, esa es la implementación de la subclase . El resultado es el bucle sin fin que está viendo.

Jonrsharpe
fuente