コンストラクタ内でのthis参照リーク問題

GUIの設計パターン」のコメントで指摘があったので補足しておきます。
Javaのコンストラクタは思った以上に複雑で、希に困った状況を引き起こします。その1つの例が「コンストラクタ内でthis参照リーク」問題です。次のようなコードがあった時、どうなるか予想できるでしょうか?

class Bar {
    Foo foo = null;
    Bar() {
    }
}

public class Foo {
    Bar bar = null;
    final String finalObj;

    Foo(Bar bar) {
        bar.foo = this;
        if (true) throw new RuntimeException();
        this.finalObj = "OK";
    }

    public static void main(String[] args) {
        Bar bar = new Bar();
        try {
            Foo foo = new Foo(bar);
        } catch (Exception e) {
        }
        System.out.println("bar=" + bar);
        System.out.println("bar.foo=" + bar.foo);
        System.out.println("bar.foo.finalObj=" + bar.foo.finalObj);
    }
}

ポイントはFooのコンストラクタで例外を搬出している点です。

実行結果

実行結果は意外にも次のようになります。

bar=Bar@6345e044
bar.foo=Foo@86c347
bar.foo.finalObj=null

Fooのコンストラクタで例外が発生しているのにFooのインスタンスがあるという奇妙な状況、おまけにfinalフィールドであるfinalObjectにnullが代入されていますが、これは明らかに想定していない状況です。

どうしてこうなった?

Javaではコンストラクタはメソッドではなく、インスタンスの生成時に実行される特殊な処理です。通常、Javaではnew演算子によってインスタンスが生成されますが、その時には次の手順でインスタンスが生成されます*1

  1. インスタンスのメモリが確保される
  2. フィールドの初期化が行われる
  3. コンストラクタが実行される
  4. インスタンスの参照を返す

コンストラクタの実行時に例外がthrowされると、インスタンスの参照は返りません。nullでもありません。なので、try-catchブロックで次のようなコードはコンパイルエラーになるのです。

Foo foo;
try {
  foo = new Foo();
} finally {
  foo.close();
}

Fooが例外をthrowする事があるので、finally節でfooが初期化されていないからです。

話を戻して今回のケースをみてみると、FooのコンストラクタにBarを渡し、Barに対してthis参照を渡しています。これはインスタンス生成の途中なのですが、インスタンスの参照自体は出来ているので有効です。しかし、問題となるのはその後に例外が発生した場合です。Fooは呼び出し元に参照が戻りませんので、本来は参照できません。しかし、Barを経由すると・・・

bar.foo=Foo@86c347

なんてことでしょう・・・、初期化が失敗したはずのFooへの参照が取得できてしまいます。フィールドも参照できますが、こんな事になります。

bar.foo.finalObj=null

あり得ない箇所でNPEが発生して現場は大混乱になるかもしれません。

どうする?

基本的にはコンストラクタ内でthis参照を使わないことがベストプラクティスですが、次のようなコードであれば問題が起きることはないとも言えます。

public class Foo {
    Bar bar = null;
    final String finalObj;

    Foo() {
        this.bar = new Bar();
        this.bar.foo = this;
        if (true) throw new RuntimeException();
        this.finalObj = "OK";
    }
}

このケースが先ほどのコードと異なるのは、BarのインスタンスをFooのコンストラクタ中で作成している点です。このコードであれば、thisの不完全なインスタンスの参照がFooの外に漏れることはありません。とはいえ、避けることができるならば避けておくのが無難です。

ありがちなパターン

this参照をコンストラクタ内で使う一番メジャーなケースはこんな所です。

class Foo extends JPanel implements MouseListener {
  Foo() {
    addMouseListener(this);
  }
  // implは省略
}

これはコンストラクタで例外が発生しても安心できるパターンですが、次のように書いて回避する事ができます。

class Foo extends JPanel {
  Foo() {
    addMouseListener(new MouseListener() {
      // implは省略
    });
  }
}

これならば別のオブジェクトだからthis参照を渡していないので、コンパイラや静的解析ツールも通ります。

GUIの例の場合

件のエントリーの例では、インターフェイスをAppFrameに実装しており、自分自身が生成した子パネルにthis参照を渡しています。

public class AppFrame extends javax.swing.JFrame implements CounterMediator {
  public AppFrame() {
    initComponents();
    this.appPanel.actionPanel.setCounterMediator(this);
  }
}

したがって、このパターンでは問題なく動作することが期待できます。NetBeansでの警告が気になるようであれば、CounterMediatorを実装した匿名クラスのインスタンスを作成しておけば回避できます。

結論

コンストラクタでのthis参照のリークは避けるべきですが、どんな時に問題となるかを理解した上でthis参照を渡す分には問題ありません。ただし、開発を進めていく中で修正した結果問題になる可能性も否定できませんから、避けるに超したことはないでしょう。

*1:関係ない処理は省略