继承和递归

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

}

现在让我们在A类中调用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)

让我们来到容易混淆的部分。现在我们在类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
5
您正在冒着默认情况下 Java 虚拟机的风险。 “定义一个方法可以被覆盖”如何比“定义一个方法可以被调用”更强烈地承诺? - Deduplicator
1
将递归方法提取到一个新的私有方法中,以获得您想要的结果。 - Onots
1
@Onots 我认为将递归方法设为静态的会更加简洁。 - ricksmt
1
如果你注意到A中的递归调用实际上是动态分派到当前对象的recursive方法,那么这很简单。如果你正在使用一个A对象,调用会带你到A.recursive(),而使用一个B对象则会带你到B.recursive()。但是B.recursive()总是调用A.recursive()。所以,如果你开始使用一个B对象,它会来回切换。 - LIProf
6个回答

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的情况下,不会发生这种情况,因为重写的方法不会被调用。


29

因为A中的recursive(i - 1);指的是this.recursive(i - 1);,即第二种情况下的B#recursive。因此,在递归函数中交替地调用superthis

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,以先到者为准。

如果在A中写入this.recursive(i-1)以获取自己的实现,那将很好,但这可能会破坏某些东西并带来其他不幸的后果,因此A中的this.recursive(i-1)调用B.recursive(),以此类推。

有一种方法可以获得预期的行为,但需要有先见之明。换句话说,您必须事先知道要让子类型的super.recursive()陷入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确保它正在调用自己的逻辑。


我想知道为什么从对象B中的recursive()函数内部调用doRecursive()函数是有效的。正如TAsk在他的答案中所写的那样,函数调用的方式是this.doRecursive(),而对象Bthis)没有doRecursive()方法,因为它在类A中被定义为private而不是protected,因此不会被继承,对吗? - Timo Denk
1
对象 B 无法调用 doRecursive()。虽然 doRecursive()private 的,但当 B 调用 super.recursive() 时,它会调用 A 中的 recursive() 实现,该实现可以访问 doRecursive() - Erick G. Hagstrom
2
这正是Bloch在Effective Java中推荐的方法,如果你绝对必须允许继承。第17条:“如果你觉得必须允许从[一个不实现标准接口的具体类]继承,一个合理的方法是确保该类从未调用其可重写方法,并记录这一事实。” - Joe

16
在类B中的super.recursive(i + 1); 显示地调用了超类的方法,因此会调用A类的recursive一次。
然后,在类A中的recursive(i - 1); 会调用类B中覆盖了类A的recursive的方法,因为它是在类B的实例上执行的。
然后,B类的recursive会显式地调用A类的recursive,依此类推。

16

实际上没有其他的方法可以实现这个。

当你调用B.recursive(10);时,它会打印出B.recursive(10),然后使用+1作为参数调用A中此方法的实现。

因此,你调用了A.recursive(11),它将打印出A.recursive(11),然后在当前实例B上使用输入参数i-1调用recursive(i-1)方法,随后它将调用B.recursive(10),然后使用+1作为参数调用超类的实现,即11,然后递归调用当前实例的recursive(i-1),其中i-110,导致你看到的循环。

这是因为如果你调用超类的实例方法,则仍将调用正在调用该方法的实例的实现。

想象一下这个情况:

 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时,那就是子类的实现。结果就是你看到的无限循环。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接