Наследование и рекурсия

Предположим, что мы имеем следующие классы:

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

}

Теперь позвоним recursive в класс A:

public class Demo {

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

}

Выход, как ожидается, будет считаться с 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)

Перейдем к запутанной части. Теперь мы называем recursive в классе B.

Ожидаемое

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...

Как это происходит? Я знаю, что это придуманный пример, но это заставляет меня задаться вопросом.

Старый вопрос с конкретный вариант использования.

Ответ 1

Ожидается. Это то, что происходит для экземпляра 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
    }

}

Таким образом, вызовы чередуются между A и B.

Это не происходит в случае экземпляра A, потому что метод overriden не будет вызываться.

Ответ 2

Потому что recursive(i - 1); в A относится к this.recursive(i - 1);, который B#recursive во втором случае. Таким образом, super и this будут вызываться в рекурсивной функции альтернативно.

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

in A

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

Ответ 3

В других ответах все объяснили существенный момент: после того, как метод экземпляра переопределен, он остается переопределенным и не возвращается, кроме как через super. B.recursive() вызывает A.recursive(). A.recursive() затем вызывает recursive(), который разрешает переопределение в B. И мы пинг-понг взад и вперед до конца вселенной или StackOverflowError, в зависимости от того, что наступит раньше.

Было бы неплохо, если бы можно было написать this.recursive(i-1) в A, чтобы получить свою собственную реализацию, но это, вероятно, сломает вещи и принесет другие неудачные последствия, поэтому this.recursive(i-1) в A вызывает B.recursive() и так д.

Существует способ получить ожидаемое поведение, но это требует предвидения. Другими словами, вы должны заранее знать, что хотите super.recursive() в подтипе A попасть в ловушку, так сказать, в реализацию 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);
    }
}

Так как A.recursive() вызывает doRecursive() и doRecursive() никогда нельзя переопределить, A гарантируется, что он вызывает свою собственную логику.

Ответ 4

super.recursive(i + 1); в классе B явно вызывает метод суперкласса, поэтому recursive из A вызывается один раз.

Затем recursive(i - 1); в классе A вызовет метод recursive в классе B, который переопределяет recursive класса A, поскольку он выполняется на экземпляре класса B.

Тогда B recursive явно вызовет A recursive и т.д.

Ответ 5

Это не может пойти иначе.

Когда вы вызываете B.recursive(10);, тогда он печатает B.recursive(10), а затем вызывает реализацию этого метода в A с помощью i+1.

Итак, вы вызываете A.recursive(11), который печатает A.recursive(11), который вызывает метод recursive(i-1); для текущего экземпляра, который является B с входным параметром i-1, поэтому он вызывает B.recursive(10), который затем вызывает супер с i+1, который является 11, который затем рекурсивно вызывает текущий экземпляр, рекурсивный с i-1, который является 10, и вы получите цикл, который вы видите здесь.

Это все потому, что если вы вызываете метод экземпляра в суперклассе, вы все равно вызовите реализацию экземпляра, на который вы его вызываете.

Представьте себе,

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

Вы получите "BARK" вместо ошибки компиляции, такой как "абстрактный метод не может быть вызван в этом экземпляре" или ошибка времени выполнения AbstractMethodError или даже pure virtual method call или что-то в этом роде. Итак, это все, чтобы поддержать polymorphism.

Ответ 6

Когда метод B экземпляр recursive вызывает реализацию класса super, то исполняемый экземпляр по-прежнему имеет значение B. Поэтому, когда реализация суперкласса вызывает recursive без дополнительной квалификации, это реализация подкласса. Результат - бесконечный цикл, который вы видите.