Dziedziczenie i rekurencja

86

Załóżmy, że mamy następujące klasy:

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

}

Teraz wezwijmy recursiveklasę A:

public class Demo {

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

}

Wynik jest, zgodnie z oczekiwaniami, odliczany od 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)

Przejdźmy do zagmatwanej części. Teraz dzwonimy recursivedo klasy B.

Oczekiwano :

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)

Rzeczywiste :

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

Jak to się stało? Wiem, że to wymyślony przykład, ale zastanawiam się.

Starsze pytanie z konkretnym przypadkiem użycia .

raupach
źródło
1
Słowo kluczowe, którego potrzebujesz, to typy statyczne i dynamiczne! powinieneś poszukać tego i trochę o tym poczytać.
ParkerHalo
1
Aby uzyskać pożądane wyniki, wyodrębnij metodę rekurencyjną do nowej metody prywatnej.
Onots
1
@ Uwagi Myślę, że statyczne metody rekurencyjne byłyby czystsze.
ricksmt
1
Jest to proste, jeśli zauważysz, że rekurencyjne wywołanie Ajest w rzeczywistości dynamicznie wysyłane do recursivemetody bieżącego obiektu. Jeśli pracujesz z Aobiektem, wywołanie przeniesie Cię do A.recursive()i z rozszerzeniemB obiektem do B.recursive(). Ale B.recursive()zawsze dzwoni A.recursive(). Tak więc, jeśli uruchomisz Bobiekt, przełącza się on tam iz powrotem.
LIProf

Odpowiedzi:

76

To jest oczekiwane. Tak dzieje się na przykład 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
    }

}

W związku z tym połączenia są wyświetlane naprzemiennie między Aa B.

Nie dzieje się tak w przypadku wystąpienia, Aponieważ nadpisana metoda nie zostanie wywołana.

Tunaki
źródło
29

Ponieważ recursive(i - 1);w Aodnosi się do tego, this.recursive(i - 1);co jest B#recursivew drugim przypadku. Tak, superi thisbędzie alternatywnie wywoływana w funkcji rekurencyjnej .

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

w A

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

Wszystkie inne odpowiedzi wyjaśniły istotną kwestię, że po zastąpieniu metody instancji pozostaje ona nadpisana i nie można jej odzyskać, jak tylko przez super. B.recursive()wywołuje A.recursive(). A.recursive()następnie wywołuje recursive(), co powoduje nadpisanie w B. I ping ponga tam iz powrotem, aż do końca wszechświata lub StackOverflowError, w zależności od tego, co nastąpi wcześniej.

Byłoby miło, gdyby ktoś mógł napisać this.recursive(i-1)w Acelu uzyskania jego własnej implementacji, ale prawdopodobnie zepsułoby to wszystko i miałoby inne niefortunne konsekwencje, tak this.recursive(i-1)w przypadku Awywołań B.recursive()i tak dalej.

Istnieje sposób na uzyskanie oczekiwanego zachowania, ale wymaga to przewidywania. Innymi słowy, musisz z góry wiedzieć, że chcesz, aby super.recursive()a podtypem programu został A, że tak powiem, uwięziony w Aimplementacji. Robi się to tak:

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

Ponieważ A.recursive()wywołuje doRecursive()i doRecursive()nigdy nie może zostać przesłonięty, Azapewnia się, że wywołuje własną logikę.

Erick G. Hagstrom
źródło
Zastanawiam się, dlaczego dzwonienie do doRecursive()środka recursive()z obiektu Bdziała. Jak napisał TAsk w swojej odpowiedzi, wywołanie funkcji działa podobnie, this.doRecursive()a Object B( this) nie ma metody, doRecursive()ponieważ jest w klasie Azdefiniowana jako privatei nie jest, protecteda zatem nie będzie dziedziczona, prawda?
Timo Denk,
1
Obiekt w ogóle Bnie może zadzwonić doRecursive(). doRecursive()jest private, tak. Ale kiedy Bwywołuje super.recursive(), wywołuje implementację recursive()in A, która ma dostęp do doRecursive().
Erick G. Hagstrom
2
To jest dokładnie podejście zalecane przez Blocha w Efektywnej Javie, jeśli absolutnie musisz zezwolić na dziedziczenie. Punkt 17: „Jeśli czujesz, że musisz zezwolić na dziedziczenie z [konkretnej klasy, która nie implementuje standardowego interfejsu], rozsądnym podejściem jest zapewnienie, że klasa nigdy nie wywoła żadnej ze swoich nadpisywalnych metod i udokumentowanie tego faktu.”
Joe
16

super.recursive(i + 1);in class Bjawnie wywołuje metodę superklasy, więc recursiveof Ajest wywoływana raz.

Następnie recursive(i - 1);w klasie A wywołałoby recursivemetodę w klasie, Bktóra przesłania recursiveklasę A, ponieważ jest wykonywana na instancji klasy B.

Następnie B„s recursivenazwałbym A” s recursivejawnie, i tak dalej.

Eran
źródło
16

To właściwie nie może iść inaczej.

Kiedy wywołujesz B.recursive(10);, drukuje, B.recursive(10)a następnie wywołuje implementację tej metody w Awith i+1.

Więc wywołujesz A.recursive(11), co drukuje, A.recursive(11)które wywołuje recursive(i-1);metodę w bieżącej instancji, która ma Bparametr wejściowy i-1, więc wywołuje B.recursive(10), a następnie wywołuje super implementację, z i+1którą jest 11, która następnie rekurencyjnie wywołuje bieżącą instancję rekurencyjną, z i-1którą jest10 , a ty zdobądź pętlę, którą tu widzisz.

To wszystko dlatego, że jeśli wywołasz metodę instancji w nadklasie, nadal będziesz wywoływał implementację instancji, w której ją wywołujesz.

Wyobraź to sobie,

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

Otrzymasz „BARK” zamiast błędu kompilacji, takiego jak „metoda abstrakcyjna nie może być wywołana w tej instancji” lub błędu w czasie wykonywania AbstractMethodErrorlub nawet pure virtual method callczegoś podobnego. A więc to wszystko po to, by wspierać polimorfizm .

EpicPandaForce
źródło
14

Kiedy metoda Binstancji recursivewywołuje superimplementację klasy, instancja, na której działa, jest nadal B. Dlatego, gdy wywołuje się implementację superklasy recursivebez dalszych kwalifikacji, jest to implementacja podklasy . Rezultatem jest niekończąca się pętla, którą widzisz.

jonrsharpe
źródło