Спадщина і рекурсія


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в клас А:

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
Щоб отримати бажані результати, витягніть рекурсивний метод до нового приватного методу.
Onots

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оскільки метод overriden не буде викликаний.


29

Тому що 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
}

в 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()і так далі.

Існує спосіб отримати очікувану поведінку, але він вимагає передбачення. Іншими словами, ви повинні заздалегідь знати, що хочете, щоб 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впевнений, що він викликає власну логіку.


Цікаво, чому виклик 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
Саме такий підхід рекомендує Блок у Effective Java, якщо ви абсолютно повинні дозволити успадкування. Пункт 17: "Якщо ви відчуваєте, що повинні дозволити успадкування від [конкретного класу, який не реалізує стандартний інтерфейс], один розумний підхід полягає у забезпеченні того, щоб клас ніколи не використовував жодних своїх переоцінюваних методів, і для документування факту".
Джо

16

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

Тоді recursive(i - 1);в класі A буде викликати recursiveметод у класі, Bякий замінює recursiveклас A, оскільки він виконується на екземплярі класу B.

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


16

Це насправді не може піти іншим шляхом.

Коли ви телефонуєте B.recursive(10);, він друкує, B.recursive(10)а потім викликає реалізацію цього методу в Awith 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щось подібне. Отже, це все для підтримки поліморфізму .


14

Коли метод Bекземпляра recursiveвикликає реалізацію superкласу, екземпляр, з яким діяли, все ще є B. Тому, коли реалізація супер-класу вимагає recursiveбез подальшої кваліфікації, це реалізація підкласу . Результат - нескінченний цикл, який ви бачите.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.