Наследование и рекурсия
Предположим, что мы имеем следующие классы:
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
без дополнительной квалификации, это реализация подкласса. Результат - бесконечный цикл, который вы видите.