Herança e recursão

86

Suponha que temos as seguintes classes:

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

}

Agora vamos chamar a recursiveclasse A:

public class Demo {

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

}

A saída é, conforme esperado, em contagem regressiva a 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)

Vamos à parte confusa. Agora chamamos a recursiveclasse 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)

Real :

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

Como isso acontece? Eu sei que este é um exemplo inventado, mas me faz pensar.

Pergunta mais antiga com um caso de uso concreto .

Raupach
fonte
1
A palavra-chave de que você precisa são tipos estáticos e dinâmicos! você deve pesquisar e ler um pouco sobre isso.
ParkerHalo
1
Para obter os resultados desejados, extraia o método recursivo para um novo método privado.
Onots
1
@Onots Eu acho que tornar os métodos recursivos estáticos seria mais limpo.
ricksmt
1
É simples se você perceber que a chamada recursiva Aé, na verdade, despachada dinamicamente para o recursivemétodo do objeto atual. Se você estiver trabalhando com um Aobjeto, a chamada o levará para A.recursive(), e com um Bobjeto, para B.recursive(). Mas B.recursive()sempre liga A.recursive(). Portanto, se você iniciar um Bobjeto, ele alterna para frente e para trás.
LIProf de

Respostas:

76

Isso é esperado. Isso é o que acontece para uma instância 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, as chamadas são alternadas entre Ae B.

Isso não acontece no caso de uma instância de Aporque o método sobrescrito não será chamado.

Tunaki
fonte
29

Porque recursive(i - 1);em Ase refere ao this.recursive(i - 1);que está B#recursiveno segundo caso. Portanto, supere thisserá chamado na função recursiva alternativamente .

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

no A

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

Todas as outras respostas explicaram o ponto essencial: uma vez que um método de instância é sobrescrito, ele permanece sobrescrito e não há como recuperá-lo, exceto por meio super. B.recursive()invoca A.recursive(). A.recursive()em seguida recursive(), invoca , que resolve a substituição em B. E jogamos pingue-pongue para frente e para trás até o fim do universo ou a StackOverflowError, o que ocorrer primeiro.

Seria bom se pudéssemos escrever this.recursive(i-1)em Aobter sua própria implementação, mas que provavelmente iria quebrar as coisas e ter outras consequências infelizes, então this.recursive(i-1)no Ainvoca B.recursive()e assim por diante.

Existe uma maneira de obter o comportamento esperado, mas requer previsão. Em outras palavras, você deve saber com antecedência que deseja que um super.recursive()em um subtipo de Afique preso, por assim dizer, na Aimplementação. É feito assim:

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

Como A.recursive()invoca doRecursive()e doRecursive()nunca pode ser substituído, Aé garantido que ele está chamando sua própria lógica.

Erick G. Hagstrom
fonte
Eu me pergunto, por que ligar para doRecursive()dentro recursive()do objeto Bfunciona. Como TAsk escreveu em sua resposta, uma chamada de função funciona como this.doRecursive()e Object B( this) não tem método doRecursive()porque está na classe Adefinida como privatee não protectede, portanto, não será herdada, certo?
Timo Denk
1
O objeto Bnão pode fazer nenhuma chamada doRecursive(). doRecursive()é private, sim. Mas quando Bchama super.recursive(), isso invoca a implementação de recursive()in A, que tem acesso a doRecursive().
Erick G. Hagstrom
2
Esta é exatamente a abordagem que Bloch recomenda em Effective Java se você absolutamente deve permitir a herança. Item 17: "Se você sentir que deve permitir a herança de [uma classe concreta que não implementa uma interface padrão], uma abordagem razoável é garantir que a classe nunca invoque nenhum de seus métodos substituíveis e documentar o fato."
Joe
16

super.recursive(i + 1);in class Bchama o método da superclasse explicitamente, portanto, recursivede Aé chamado uma vez.

Então, recursive(i - 1);na classe A chamaria o recursivemétodo na classe Bque sobrescreve a recursiveclasse A, uma vez que é executado em uma instância da classe B.

Então, B's recursivechamaria A' s recursiveexplicitamente e assim por diante.

Eran
fonte
16

Isso realmente não pode acontecer de outra maneira.

Quando você chama B.recursive(10);, ele imprime B.recursive(10)e chama a implementação desse método Acom i+1.

Então você chama A.recursive(11), que imprime A.recursive(11)que chama o recursive(i-1);método na instância atual que está Bcom o parâmetro de entrada i-1, então chama B.recursive(10), que então chama a superimplementação com i+1que é 11, que então chama recursivamente a instância atual recursiva com i-1que é 10, e você pegue o loop que você vê aqui.

Isso tudo porque, se você chamar o método da instância na superclasse, ainda chamará a implementação da instância na qual está chamando.

Imagina isto,

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

Você obterá "BARK" em vez de um erro de compilação, como "o método abstrato não pode ser chamado nesta instância" ou um erro de tempo de execução AbstractMethodErrorou mesmo pure virtual method callou algo parecido. Portanto, tudo isso é para oferecer suporte ao polimorfismo .

EpicPandaForce
fonte
14

Quando Bo recursivemétodo de uma instância chama a superimplementação da classe, a instância que está sendo atuada ainda é de B. Portanto, quando a implementação da superclasse chama recursivesem qualificação adicional, essa é a implementação da subclasse . O resultado é o loop sem fim que você está vendo.

Jonrsharpe
fonte