継承と再帰


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 2015


1
目的の結果を得るには、再帰メソッドを新しいプライベートメソッドに抽出します。
Onots 2015

1
@Onots再帰メソッドを静的にする方がクリーンだと思います。
ricksmt 2015

1
再帰呼び出しAが実際recursive現在のオブジェクトのメソッドに動的にディスパッチされていることに気付いた場合は簡単です。Aオブジェクトを操作している場合、呼び出しによりA.recursive()、に移動し、Bオブジェクトを使用してに移動しB.recursive()ます。ただし、B.recursive()常にを呼び出しますA.recursive()。したがって、Bオブジェクトを開始すると、オブジェクトは前後に切り替わります。
LIProf 2015

回答:


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オーバーライドされたメソッドが呼び出されないため、のインスタンスの場合、これは発生しません。


29

recursive(i - 1);inは、2番目のケースのどちらをA参照するためです。だから、とに呼び出される再帰関数で交互にthis.recursive(i - 1);B#recursivesuperthis

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

他の回答はすべて本質的なポイントを説明しています。インスタンスメソッドがオーバーライドされると、オーバーライドされたままになり、を介さない限り元に戻すことはできませんsuperB.recursive()を呼び出しますA.recursive()A.recursive()次に、を呼び出しますrecursive()。これは、のオーバーライドに解決されますB。そして、宇宙の終焉StackOverflowErrorか、どちらか早い方までピンポンを行ったり来たりします。

1を書くことができればいいだろう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 Bthis)にはメソッドがありませんdoRecursive()。これは、Aとして定義されたクラスにあり、継承されprivateないprotectedためです。
ティモデンク2016年

1
オブジェクトBはまったく呼び出すことができませんdoRecursive()doRecursive()であるprivateはい、。しかし、時にB呼び出しsuper.recursive()、その呼び出すの実装recursive()Aへのアクセスを持っています、doRecursive()
Erick G. Hagstrom 2016年

2
これは、継承を絶対に許可する必要がある場合に、EffectiveJavaでBlochが推奨するアプローチとまったく同じです。項目17:「[標準インターフェースを実装しない具象クラス]からの継承を許可する必要があると思われる場合、合理的なアプローチの1つは、クラスがオーバーライド可能なメソッドを呼び出さないようにし、事実を文書化することです。」
ジョー

16

super.recursive(i + 1);in classBはスーパークラスのメソッドを明示的に呼び出すためrecursive、ofAは1回呼び出されます。

その後、recursive(i - 1);クラスAが呼び出すことになるrecursiveクラスのメソッドBオーバーライドrecursiveクラスのをA、それがクラスのインスタンスで実行されるので、B

次に、B'srecursiveA' 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.