メモ置き場

メモ置き場です.開発したものや調べたことについて書きます.

[tex: ]

Javaでスーパークラスにサブクラスを返すメソッドを用意してみる

Javaの勉強をしていて,ふと疑問になったことがある.

JavaにはObjectクラスというものがあり,ObjectクラスにはにはStringを返すtoStringメソッドがある.一方で,StringクラスはスーパークラスとしてObjectクラスを持つ.つまり,Stringクラスの定義にはObjectクラスが必要で,Objectクラスの定義にはStringクラスが含まれているという状況だ.これは循環定義になっていないだろうか?

これについて簡単な実験をしてみた.

スーパークラスにサブクラスを返すメソッドを作る

次のようなコードを書いてみた.
クラスAは,クラスBという別のクラスを返すメソッドreturnBを持つ.クラスBはAクラスを継承したクラスである.

// A.java
public class A {
    private String name;

    public A(String name){
        this.name = name;
    }

    public A(){
        this.name = "NONAME";
    }

    public B returnB(){
        B b = new B("B generate from " + name);
        return b;
    }

    public void sayHello(){
        System.out.println("Hello from A:  " + name);
    }
}
// B.java
public class B extends A{
    private String B_name;

    public B(String name){
        this.B_name = name;
    }

    public void printName() {
        System.out.println(B_name);
    }

    @Override
    public void sayHello(){
        System.out.println("Hello from B  " + B_name);
    }
}
// Main.java
public class Main {
    public static void main(String[] args) {
        A a = new A("new A");
        B b = a.returnB();

        b.printName();

        a.sayHello();
        b.sayHello();

	a.returnB().returnB().returnB().sayHello();
	// サブクラスからスーパークラスのメソッドを呼び出せるので,BのインスタンスからAのメソッドであるreturnB()を呼び出し,さらにreturnB()を呼び出し…
	// といったこともできる.

    }
}
実行結果
B generate from new A
Hello from A:  new A
Hello from B  B generate from new A

クラスAの中に,サブクラスのBを返すメソッドを作った.またクラスAにsayHelloというメソッドを定義しBでオーバーライドしている.
クラスAの定義にはクラスBの情報が必要であり,クラスBはAを継承しているので,互いに依存した定義になっているように見える.

この2つのクラスに対して,mainでクラスAのインスタンスaを作り,aからreturnBメソッドを呼び出してBのインスタンスを生成している.生成されたインスタンスはbに代入する.
最後にa,bからメソッドを呼び出す,というプログラムである.

Javaコンパイルしてみるとちゃんと書いた通りに動く.一見循環定義になっているように見えるのになんでだろう.
スーパークラスでサブクラスに関する情報を持っている,といった状況についての解説が見当たらなかったので,わかる方がいらっしゃったら是非コメント欄やツイッターのリプライ等で教えてください.

恐らくだだが,「クラスAの定義にはクラスBの情報が必要」ではないのではないかと思っている.必要でないというのは,クラスBの情報がなくてもクラスAのインスンタンスが生成できるといいう意味である.クラスBの情報が必要になるのはクラスAのreturnBメソッドを呼び,クラスBのインスタンスを返す時だけである.この時クラスBのインスタンスはreturnB内のローカル変数として扱われるため,Aのインスタンスから直接アクセスできる位置に確保されないんだと思う.また,メソッドはインスタンスへの参照を返すため,クラスAのインスタンス生成時には「他のインスタンスへの参照を返す関数」ということだけわかっていればいいのだろう.

スーパークラスにサブクラスのインスタンスをフィールドとして持たせる

スーパークラスに,サブクラスを呼び出すメソッドがあるプログラムはちゃんと動くことがわかった.一方で,スーパークラスにサブクラスのインスタンスをフィールドとして持たせるとエラーになる.

public class A {
    private String name;
    private B b; // サブクラスのインスタンス

    public A(String name){
        this.name = name;
        System.out.println("super class");
        this.b = new B("BBBBB");
	// AのコンストラクタでBのコンストラクタを呼び出してインスタンス化
    }

    public A(){
        this("NONAME");
    }

    public B returnB(){
        B b = new B("B generate from " + name);
        return b;
    }

    public void sayHello(){
        System.out.println("Hello from A:  " + name);
    }
}
public class B extends A{
    private String B_name;

    public B(String name){
        super(name);
        this.B_name = name;
    }

    public void printName() {
        System.out.println(B_name);
    }

    @Override
    public void sayHello(){
        System.out.println("Hello from B  " + B_name);
    }
}
public class Main {
    public static void main(String[] args) {
        A a = new A("aaa");
    }
}
実行結果
super class
super class
super class
super class
…
Exception in thread "main" java.lang.StackOverflowError

上記コードだと,Aをインスタンス化しようとしてAのコンストラクタを呼び出す.AのコンストラクタではサブクラスのコンストラクタBが呼び出される.Bのコンストラクタが呼び出されるとスーパークラスのAのコンストラクタが呼び出される.すると,AのコンストラクタからBのコンストラクタが呼び出され,またAのコンストラクタが呼び出され… というようにループしてエラーとなる.

AのコンストラクタでBのインスタンス化を行わないようにコードを変更してみると

public class A {
    private String name;
    private B b; // サブクラスのインスタンス

    public A(String name){
        this.name = name;
        System.out.println("super class");
    }

    public A(){
        this("NONAME");
    }

    public B returnB(){
        B b = new B("B generate from " + name);
        return b;
    }

    public void genB(){
        this.b = new B(this.name + " b");
    }
    // public メソッドを用意してその中でbにインスタンスを代入する.

    public void sayHello(){
        System.out.println("Hello from A:  " + name);
    }
}
// Main.java
// B.java は変更なし
public class Main {
    public static void main(String[] args) {
        A a = new A("aaa");

        a.genB();
	// genB()を呼び出さないとAのフィールドbにはインスタンスが入っておらずnullポインタが返る.
        B b = a.getB();
	// Aのフィールドbをゲット
	b.sayHello();
    }
}

このように変更すると,上記のようにコンストラクタ間で呼び出しあってループし続けるというバグはなくなる.
しかし,Aをインスタンス化した段階でAのフィールドbにインスタンスが参照されておらず,genBメソッドを呼び忘れてnullを参照してしまう可能性を含んでおり,よくない.普通クラスフィールドはコンストラクタを読んだ段階で全て初期化されておくべきであり,上記のようなコードは避けるべきだ.当たり前だが,スーパークラスにサブクラスのフィールドを持たせることはやってはいけない.

結論

このことについて公式のドキュメント等があったら教えてください.