Kế thừa và đệ quy


86

Giả sử chúng ta có các lớp sau:

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

}

Bây giờ, hãy gọi recursivetrong lớp A:

public class Demo {

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

}

Đầu ra, như mong đợi là đếm ngược từ 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)

Hãy đến phần khó hiểu. Bây giờ chúng ta gọi recursivevào lớp B.

Dự kiến :

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)

Thực tế :

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

Làm thế nào điều này xảy ra? Tôi biết đây là một ví dụ sáng tạo, nhưng nó khiến tôi tự hỏi.

Câu hỏi cũ hơn với một trường hợp sử dụng cụ thể .


1
Từ khóa bạn cần là loại tĩnh và loại động! bạn nên tìm kiếm và đọc một chút về nó.
ParkerHalo


1
Để nhận được kết quả mong muốn của bạn, hãy giải nén phương thức đệ quy sang một phương thức riêng tư mới.
Onots

1
@Onots Tôi nghĩ rằng làm cho các phương thức đệ quy tĩnh sẽ sạch hơn.
ricksmt

1
Thật đơn giản nếu bạn nhận thấy rằng lời gọi đệ quy trong Athực sự được gửi động đến recursivephương thức của đối tượng hiện tại. Nếu bạn đang làm việc với một Ađối tượng, cuộc gọi sẽ đưa bạn đến A.recursive()và với một Bđối tượng, đến B.recursive(). Nhưng B.recursive()luôn luôn gọi A.recursive(). Vì vậy, nếu bạn bắt đầu một Bđối tượng, nó sẽ chuyển đổi qua lại.
LIProf

Câu trả lời:


76

Điều này được mong đợi. Đây là những gì xảy ra cho một ví dụ của 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
    }

}

Như vậy, các cuộc gọi xen kẽ giữa AB.

Điều này không xảy ra trong trường hợp của Abởi vì phương thức ghi đè sẽ không được gọi.


29

Bởi vì recursive(i - 1);trong Ađề cập đến this.recursive(i - 1);B#recursivetrong trường hợp thứ hai. Vì vậy, superthissẽ được gọi trong hàm đệ quy một cách khác .

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

trong A

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

27

Các câu trả lời khác đều đã giải thích điểm cốt yếu, rằng một khi một phương thức thể hiện bị ghi đè, nó sẽ bị ghi đè và không thể lấy lại được ngoại trừ thông qua super. B.recursive()cầu khẩn A.recursive(). A.recursive()sau đó gọi recursive(), giải quyết ghi đè trong B. Và chúng ta chơi bóng bàn qua lại cho đến tận cùng của vũ trụ hoặc a StackOverflowError, tùy điều kiện nào đến trước.

Sẽ thật tuyệt nếu người ta có thể viết this.recursive(i-1)vào Ađể có được phần triển khai của riêng nó, nhưng điều đó có thể sẽ phá vỡ mọi thứ và gây ra những hậu quả đáng tiếc khác, do đó, this.recursive(i-1)trong những lời mời Agọi B.recursive(), v.v.

Có một cách để có được hành vi mong đợi, nhưng nó đòi hỏi tầm nhìn xa. Nói cách khác, bạn phải biết trước rằng bạn muốn có super.recursive()một kiểu phụ Abị mắc kẹt, có thể nói là trong quá trình Atriển khai. Nó được thực hiện như vậy:

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()gọi ra doRecursive()doRecursive()không bao giờ có thể bị ghi đè, Ahãy yên tâm rằng nó đang gọi logic của chính nó.


Tôi tự hỏi, tại sao gọi doRecursive()bên trong recursive()từ đối tượng Bhoạt động. Như TAsk đã viết trong câu trả lời của mình, một lời gọi hàm hoạt động giống như vậy this.doRecursive()và Object B( this) không có phương thức nào doRecursive()vì nó nằm trong lớp Ađược định nghĩa là privatevà không protectedvà do đó sẽ không được kế thừa, phải không?
Timo Denk

1
Đối tượng Bkhông thể gọi doRecursive()gì cả. doRecursive()private, có. Nhưng khi Bcác cuộc gọi super.recursive(), điều đó gọi việc triển khai recursive()in A, có quyền truy cập vào doRecursive().
Erick G. Hagstrom,

2
Đây chính xác là cách tiếp cận mà Bloch đề xuất trong Java hiệu quả nếu bạn hoàn toàn phải cho phép kế thừa. Mục 17: "Nếu bạn cảm thấy bạn phải cho phép kế thừa từ [một lớp cụ thể không triển khai giao diện chuẩn], một cách tiếp cận hợp lý là đảm bảo rằng lớp không bao giờ gọi bất kỳ phương thức có thể ghi đè nào của nó và ghi lại thực tế."
Joe

16

super.recursive(i + 1);trong lớp Bgọi phương pháp siêu lớp của một cách rõ ràng, vì vậy recursivetrong Ađược gọi là một lần.

Sau đó, recursive(i - 1);trong lớp A sẽ gọi recursivephương thức trong lớp Bghi đè recursivelớp A, vì nó được thực thi trên một thể hiện của lớp B.

Sau đó, B's recursivesẽ gọi A' s recursivemột cách rõ ràng, v.v.


16

Điều đó thực sự không thể đi theo bất kỳ cách nào khác.

Khi bạn gọi B.recursive(10);, sau đó nó sẽ in B.recursive(10)sau đó gọi việc triển khai phương thức này trong Awith i+1.

Vì vậy, bạn gọi A.recursive(11), mà in A.recursive(11)trong đó kêu gọi các recursive(i-1);phương pháp trên dụ hiện tại mà là Bvới tham số đầu vào i-1, vì vậy nó gọi B.recursive(10), sau đó kêu gọi việc thực hiện siêu với i+1đó là 11, sau đó đệ quy gọi là đệ quy Ví dụ hiện tại với i-1đó là 10, và bạn sẽ lấy vòng lặp mà bạn thấy ở đây.

Tất cả là vì nếu bạn gọi phương thức của cá thể trong lớp cha, bạn sẽ vẫn gọi việc triển khai cá thể mà bạn đang gọi nó.

Hãy tưởng tượng điều này,

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

Bạn sẽ nhận được "BARK" thay vì lỗi biên dịch chẳng hạn như "phương thức trừu tượng không thể được gọi trên phiên bản này" hoặc lỗi thời gian chạy AbstractMethodErrorhoặc thậm chí pure virtual method callhoặc tương tự như vậy. Vì vậy, đây là tất cả để hỗ trợ tính đa hình .


14

Khi phương thức Bcủa một cá thể recursivegọi việc supertriển khai lớp, thì cá thể đang được thực hiện vẫn là của B. Do đó, khi việc triển khai của lớp siêu gọi recursivemà không có đủ điều kiện khác, đó là việc triển khai lớp con . Kết quả là vòng lặp không bao giờ kết thúc mà bạn đang thấy.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.