การสืบทอดและการเรียกซ้ำ


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
เพื่อให้ได้ผลลัพธ์ที่คุณต้องการให้ดึงวิธีการเรียกซ้ำเป็นวิธีส่วนตัวใหม่
58

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
    }

}

เช่นสายจะสลับกันระหว่างและAB

สิ่งนี้ไม่เกิดขึ้นในกรณีของอินสแตนซ์Aเนื่องจากจะไม่มีการเรียกเมธอด overriden


29

เนื่องจากrecursive(i - 1);ในการAอ้างถึงthis.recursive(i - 1);ซึ่งอยู่B#recursiveในกรณีที่สอง ดังนั้นsuperและthisจะถูกเรียกในrecursiveฟังก์ชั่นอีกทางเลือกหนึ่ง

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
Object Bไม่สามารถโทรdoRecursive()ได้เลย doRecursive()คือprivateใช่ แต่เมื่อBสายsuper.recursive()ที่จะเรียกการดำเนินการrecursive()ในซึ่งมีการเข้าถึงA doRecursive()
Erick G.Hagstrom

2
นี่เป็นแนวทางที่ Bloch แนะนำใน Effective Java หากคุณต้องอนุญาตการสืบทอดอย่างแน่นอน ข้อ 17: "ถ้าคุณรู้สึกว่าคุณต้องยอมให้มีการสืบทอดจาก [คลาสคอนกรีตที่ไม่ได้ใช้อินเทอร์เฟซมาตรฐาน] วิธีการหนึ่งที่สมเหตุสมผลคือเพื่อให้แน่ใจว่าคลาสจะไม่เรียกใช้วิธีการใด ๆ ที่สามารถลบล้างได้และเพื่อบันทึกข้อเท็จจริง"
โจ

16

super.recursive(i + 1);ในชั้นเรียนBเรียกวิธีชั้นซุปเปอร์อย่างชัดเจนดังนั้นrecursiveการAที่เรียกว่าครั้งเดียว

จากนั้นrecursive(i - 1);ในชั้นเรียนจะเรียกrecursiveวิธีการในชั้นเรียนBซึ่งแทนที่recursiveของการเรียนเพราะมันจะถูกดำเนินการในอินสแตนซ์ของคลาสAB

จากนั้นB's recursiveจะเรียกA' s recursiveอย่างชัดเจนและอื่น ๆ


16

ที่จริงไปทางอื่นไม่ได้

เมื่อคุณโทรB.recursive(10);แล้วมันพิมพ์B.recursive(10)แล้วเรียกการดำเนินการตามวิธีการนี้ในด้วยAi+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ได้โดยไม่ต้องมีคุณสมบัติเพิ่มเติมที่ดำเนินการประเภทรอง ผลลัพธ์คือลูปที่ไม่มีวันสิ้นสุดที่คุณเห็น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.