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


86

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

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)

Актуально :

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

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

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


1
Ключевое слово, которое вам нужно, - это статические и динамические типы! вам следует поискать это и немного прочитать об этом.
ParkerHalo


1
Чтобы получить желаемые результаты, извлеките рекурсивный метод в новый частный метод.
Онотс

1
@Onots Я думаю, что сделать рекурсивные методы статическими было бы чище.
ricksmt

1
Это просто , если вы заметили , что рекурсивный вызов в Aдействительности динамически отправляются в по recursiveметоду текущего объекта. Если вы работаете с Aобъектом, то вызов принимает вас A.recursive(), и с Bобъектом, к B.recursive(). Но B.recursive()всегда звонит A.recursive(). Итак, если вы запускаете Bобъект, он переключается вперед и назад.
LIProf

Ответы:


76

Это ожидаемо. Вот что происходит с экземпляром 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потому что метод переопределения не будет вызван.


29

Потому что recursive(i - 1);in 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
}

в A

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

27

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

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

Есть способ добиться ожидаемого поведения, но он требует предвидения. Другими словами, вы должны заранее знать, что вы хотите, чтобы a super.recursive()in подтипа 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он уверен, что вызывает свою собственную логику.


Интересно, почему работает вызов doRecursive()внутрь recursive()из объекта B. Как написал TAsk в своем ответе, вызов функции работает так же, как this.doRecursive()и у Object B( this) нет метода, doRecursive()потому что он находится в классе, Aопределенном как, privateа не, protectedи поэтому не будет унаследован, верно?
Timo Denk

1
Объект Bвообще не может позвонить doRecursive(). doRecursive()есть private, да. Но при Bвызове super.recursive()это вызывает реализацию recursive()in A, к которой есть доступ doRecursive().
Эрик Г. Хагстром,

2
Это именно тот подход, который Блох рекомендует в «Эффективной Java», если вам абсолютно необходимо разрешить наследование. Правило 17: «Если вы чувствуете, что должны разрешить наследование от [конкретного класса, который не реализует стандартный интерфейс], один разумный подход состоит в том, чтобы гарантировать, что класс никогда не будет вызывать какие-либо из своих переопределяемых методов, и задокументировать этот факт».
Джо

16

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

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

Тогда B«s recursiveназвали бы A» S в recursiveявном виде, и так далее.


16

Иначе и быть не может.

Когда вы вызываете 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которым is 11, которая затем рекурсивно вызывает рекурсивный текущий экземпляр с i-1которым is 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или чего-то подобного. Итак, это все для поддержки полиморфизма .


14

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.