JPCERT コーディネーションセンター

TSM01-J. オブジェクトの構築時にthis参照を逸出させない

Java言語仕様の15.8.3節「this には、以下のように記載されている[JLS 2015]。

一次式として用いられた場合、this キーワードはインスタンスメソッドの起動(§15.12)が行われたオブジェクト、または構築中のオブジェクトへの参照値を表す....

this の型は、this キーワードが記述されているクラスまたはインターフェイス型 T である....

実行時に実際のオブジェクトが参照するクラスは、T (T がクラス型である場合) あるいは T のサブクラスとなる。

現在のスコープを越えて this 参照がアクセス可能な状態になることを逸出(escape)と呼ぶ。よくある this 参照の逸出例としては、以下のようなものがある。

このルールでは、競合状態や不適切な初期化など、オブジェクトの構築時に this 参照の逸出を許してしまうことでもたらされる潜在的な影響について記述している。たとえば、final 宣言されたフィールドは、通常、どのスレッドからも完全に初期化された状態で読み取られる。しかし、オブジェクトの構築時に this 参照の逸出を許すと、final 宣言されたフィールドであっても、初期化されていない状態あるいは初期化途中の状態で、他のスレッドからアクセス可能になってしまう。「TSM03-J. 初期化が完了していないオブジェクトを公開しない」は、安全な公開のための様々な仕組みが提供する保証について解説しているが、これらは本ルールに適合していることが前提となっている。それゆえ、プログラムではオブジェクト構築時に this 参照を逸出させてはいけない。

一般に、this 参照が実行時のコンテキストのスコープを越えて逸出してしまう場合を検出することは重要である。とくに、public 宣言された変数やメソッドは精査されるべきである。

違反コード (初期化前に this 参照を公開)

以下の違反コード例では、public static volatile 宣言されたクラスフィールドに this 参照を格納することで、初期化処理が完了する前に this 参照を公開してしまっている。そのため、他のスレッドは初期化が完了していない Publisher インスタンスにアクセスできてしまう。

final class Publisher {
  public static volatile Publisher published;
  int num;

  Publisher(int number) {
    published = this;
    // 初期化処理
    this.num = number;
    // ...
  }
}

また、オブジェクトの初期化と構築処理が、コンストラクタ内で行われるセキュリティーチェック処理に依存している場合、初期化を完了していないインスタンスを獲得した信頼できない呼出し元は、セキュリティチェックを回避できる(詳しくは「OBJ11-J. コンストラクタが例外をスローする場合には細心の注意を払う」を参照)。

違反コード (volatile 宣言されていない public static フィールド)

以下の違反コード例では、this 参照の公開をコンストラクタの最後のステートメントに移動している。しかし、published フィールドの公開範囲は public であり、また、volatile 宣言されていないため、あいかわらず脆弱である。

final class Publisher {
  public static Publisher published;
  int num;

  Publisher(int number) {
    // 初期化処理
    this.num = number;
    // ...
    published = this;
  }
}

published フィールドは volatile 宣言も final 宣言もされていないため、コンストラクタ内のステートメントの実行順序はコンパイラにより変更されることがある。その場合、初期化処理の部分の実行前に this 参照が公開されてしまう可能性がある。

適合コード (volatile フィールドと初期化後の公開)

以下の適合コードでは、published フィールドを volatile 宣言するとともにアクセス範囲をパッケージプライベートに限定している。そのため、外部パッケージスコープからは this 参照を獲得することはできない。

final class Publisher {
  static volatile Publisher published;
  int num;

  Publisher(int number) {
    // 初期化処理
    this.num = number;
    // ...
    published = this;
  }
}

上記コードでは、コンストラクタは初期化の完了後に this 参照を公開している。しかし Publisher クラスをインスタンス化する場合には、num フィールドについても、初期化される前のデフォルト値を読み取られないように注意しなければならない。初期化が完了していない状態の num フィールドを読み取られる場合は「TSM03-J. 初期化が完了していないオブジェクトを公開しない」への違反となる。これを防ぐには、Publisher への参照を保持するフィールドを volatile 宣言するなどの対応が必要になるであろう。

published フィールドが volatile 宣言されていない場合、初期化処理内容の実行順序は変更されうる。しかし Java コンパイラは、フィールドの修飾子として volatile と final 両方同時に宣言することを許していない。

Publisher クラスも final 宣言する必要がある。さもないと、サブクラスが Publisher クラスのコンストラクタを呼び出し、自身の初期化が完了する前に this 参照を公開することもありうる。

適合コード (public static ファクトリメソッド)

以下の適合コードでは、内部メンバフィールドをなくし、Publisher インスタンスを生成して返す newInstance() ファクトリメソッドを提供している。

final class Publisher {
  final int num;

  private Publisher(int number) {
    // 初期化処理
    this.num = number;
  }

  public static Publisher newInstance(int number) {
    Publisher published = new Publisher(number);
    return published;
  }
}

このアプローチにより、不完全な Publisher インスタンスがスレッドから読み取られないようになる。num フィールドも final 宣言することでクラスを不変にし、初期化が完了していないオブジェクトを獲得される可能性を排除している。

違反コード (例外ハンドラ)

ここで紹介する違反コード例では、以下に示す ExceptionReporter インタフェースを実装している。

public interface ExceptionReporter {
  public void setExceptionReporter(ExceptionReporter er);
  public void report(Throwable exception);
}

このインタフェースを以下の DefaultExceptionReporter クラスで実装する。DefaultExceptionReporter クラスは、センシティブな情報をすべて除去したうえで、例外を報告する (詳しくは「ERR00-J. チェック例外を抑制あるいは無視しない」を参照)。

DefaultExceptionReporterクラスのコンストラクタが、オブジェクトの構築が完了する前に this 参照を公開してしまうことを、以下で説明する。コンストラクタの最後のステートメント(er.setExceptionReporter(this))で例外レポーターを設定しているところが問題の部分であるが、コンストラクタの最後のステートメントなので、問題ないものと誤解してしまいやすい。

// DefaultExceptionReporter クラス
public class DefaultExceptionReporter implements ExceptionReporter {
  public DefaultExceptionReporter(ExceptionReporter er) {
    // 初期化処理の実行
    // 誤って「this」参照を公開してしまう
    er.setExceptionReporter(this);
  }

  // setExceptionReporter() メソッドと report() メソッドの実装
}

以下に示す MyExceptionReporter クラスは、DefaultExceptionReporter クラスを継承し、例外を報告する前に重要なメッセージを記録するログ出力の仕組みを追加している。

// MyExceptionReporter クラスは DefaultExceptionReporter を継承している
public class MyExceptionReporter extends DefaultExceptionReporter {
  private final Logger logger;

  public MyExceptionReporter(ExceptionReporter er) {
    super(er); // 上位クラスのコンストラクタを呼ぶ
    // デフォルトのloggerを獲得
    logger = Logger.getLogger("com.organization.Log");
  }

  public void report(Throwable t) {
    logger.log(Level.FINEST,"Loggable exception occurred", t);
  }
}

MyExceptionReporter クラスのコンストラクタは上位クラス DefaultExceptionReporter のコンストラクタを呼び出している。そして DefaultExceptionReporter クラスのコンストラクタは、MyExceptionReporter 自体の初期化が完了する前にそのインスタンスを例外レポーターとして公開してしまう。また、サブクラスの初期化処理では、デフォルトのロガーのインスタンスを取得していることに注意。例外レポーターを公開するということは、その時点から例外の受信と処理が始まるということを意味する。

MyExceptionReporter クラスのコンストラクタの中で Logger.getLogger() が呼び出される前に例外が発生した場合、その内容は記録されない。初期化されていない logger フィールドを参照するため NullPointerException が発生し、その例外自体もログに出力されることなく消費されてしまうからである。

この問題は、例外の発生と MyExceptionReporter の初期化との競合状態に起因している。例外の発生が早過ぎると、初期化が完了する前の MyExceptionReporter オブジェクトを参照することになる。logger は final 宣言されており、初期化されていない状態で参照されることは想定されていないので、この振舞いは非常に分かりにくい。

同様の問題は、イベントリスナーの公開が早過ぎる場合にも発生しうる。つまり、サブクラスが初期化を完了する前に、イベント通知を受信してしまう問題である。

適合コード

以下の適合コードでは、DefaultExceptionReporter クラスのコンストラクタで this 参照を公開する代わりに、例外レポーターを設定する publishExceptionReporter() メソッドを追加している。サブクラスの初期化が完了してから publishExceptionReporter() メソッドを呼び出すようにすればよい。

public class DefaultExceptionReporter implements ExceptionReporter {
  public DefaultExceptionReporter(ExceptionReporter er) {
    // ...
  }

  // サブクラスの初期化処理の完了後に呼び出す必要がある
  public void publishExceptionReporter() {
    setExceptionReporter(this); // この例外レポータを登録
  }

  // setExceptionReporter() メソッドと report() メソッドの実装
}

MyExceptionReporter サブクラスは publishExceptionReporter() メソッドを継承しているので、MyExceptionReporter をインスタンス化した呼出し元は、初期化の完了後、そのインスタンスを使用して、例外レポーターを設定することができる。

// MyExceptionReporter クラスは、 DefaultExceptionReporter クラスを継承している
public class MyExceptionReporter extends DefaultExceptionReporter {
  private final Logger logger;

  public MyExceptionReporter(ExceptionReporter er) {
    super(er); // 上位クラスのコンストラクタ呼出し
    logger = Logger.getLogger("com.organization.Log");
  }
  // publishExceptionReporter() メソッドと
  // setExceptionReporter() メソッドと report() メソッドの
  // 実装が継承されている
}

このアプローチにより、コンストラクタがサブクラスのインスタンスを初期化してログ出力が可能になるまでは、例外レポーターが設定されることはない。

違反コード (内部クラス)

内部クラスは、外部クラスのオブジェクトの this 参照のコピーを保持しており、this 参照がスコープ外に逸出する原因となりうる[Goetz 2002]。以下の違反コード例では DefaultExceptionReporter クラスの異なる実装例を示しており、コンストラクタは、匿名内部クラスを使用して例外レポーターを公開している。

public class DefaultExceptionReporter implements ExceptionReporter {
  public DefaultExceptionReporter(ExceptionReporter er) {
    er.setExceptionReporter(new ExceptionReporter() {
        public void report(Throwable t) {
          // 例外を通知
        }
        public void setExceptionReporter(ExceptionReporter er) {
          // ExceptionReporter を登録
        }
    });
  }
  // setExceptionReporter() メソッドと report() メソッドのデフォルトの実装
}

内部クラスが外部クラスの this 参照を公開してしまうため、他のスレッドは、外部クラスの this 参照にアクセスすることができる。また、このクラスのサブクラスをつくると、前述の違反コード(例外ハンドラ)で説明した問題も発生する。

適合コード

private 宣言されたコンストラクタと public static 宣言されたファクトリメソッドを用いることで、例外レポーターをコンストラクタ内部から安全に公開することができる[Goetz 2006a]。

public class DefaultExceptionReporter implements ExceptionReporter {
  private final ExceptionReporter defaultER;

  private DefaultExceptionReporter(ExceptionReporter excr) {
    defaultER = new ExceptionReporter() {
      public void report(Throwable t) {
        // 例外を通知
      }
      public void setExceptionReporter(ExceptionReporter er) {
        // ExceptionReporter を登録
      }
    };
  }

  public static DefaultExceptionReporter newInstance(
                ExceptionReporter excr) {
    DefaultExceptionReporter der = new DefaultExceptionReporter(excr);
    excr.setExceptionReporter(der.defaultER);
    return der;
  }
  // setExceptionReporter() メソッドと report() メソッドのデフォルトの実装
}

コンストラクタを private 宣言することで、信頼できないコードがクラスのインスタンスを作成できないようにし、this 参照の逸出を防止している。また、新たなインスタンスの作成に public static 宣言されたファクトリメソッドを使用することで、信頼できないコードによってオブジェクトの内部状態を操作されたり、初期化が完了していないオブジェクトが公開されることを防いでいる(詳しくは「TSM03-J. 初期化が完了していないオブジェクトを公開しない」を参照)。

違反コード (スレッド)

以下の違反コード例では、コンストラクタ内部からスレッドを開始している。

final class ThreadStarter implements Runnable {
  public ThreadStarter() {
    Thread thread = new Thread(this);
    thread.start();
  }

  @Override public void run() {
    // ...
  }
}

この例では、新たなスレッドから、実行中のオブジェクトの this 参照にアクセス可能である [Goetz 2002、Goetz 2006a]。Thread() コンストラクタは、ThreadStarter クラスから見てよそ者メソッドである。

適合コード (スレッド)

以下の適合コードでは、コンストラクタではなく、他のメソッド内でスレッドを作成し、開始している。

final class ThreadStarter implements Runnable {
  public void startThread() {
    Thread thread = new Thread(this);
    thread.start();
  }

  @Override public void run() {
    // ...
  }
}

例外

TSM01-J-EX0: オブジェクトの構築が完了するまでスレッドを開始しないのであれば、コンストラクタ内でスレッドを作成しても安全である。スレッドの start() メソッドの呼出しは、スレッド内で定義されている如何なる動作よりも事前に発生するからである[JLS 2015]。

以下のコード例では、this を参照するスレッドがコンストラクタで作成されているが、startThread() メソッドから start() メソッドが呼び出されるまでスレッドは開始しない[Goetz 2002、Goetz 2006a]。

final class ThreadStarter implements Runnable {
  Thread thread;

  public ThreadStarter() {
    thread = new Thread(this);
  }

  public void startThread() {
    thread.start();
  }

  @Override public void run() {
    // ...
  }
}

TSM01-J-EX1:TSM02-J. クラスの初期化中にバックグラウンドスレッドを使用しない」で説明している ObjectPreserver パターン [Grand 2002] は安全であり、使ってもよい。

リスク評価

this 参照の逸出を許してしまうと、不適切な初期化および実行時例外につながる危険がある。

ルール

深刻度

可能性

修正コスト

優先度

レベル

TSM01-J

P4

L3

自動検出
ツール バージョン チェッカー 説明
Parasoft Jtest 10.3 TRS.CTRE 実装済み
参考文献

[Goetz 2002]

 

[Goetz 2006a]

Section 3.2, "Publication and Escape"

[Grand 2002]

Chapter 5, "Creational Patterns, Singleton"

[JLS 2015] §15.8.3, "this"
翻訳元

これは以下のページを翻訳したものです。

TSM01-J. Do not let the this reference escape during object construction (revision 190)

Top へ

Topへ
最新情報(RSSメーリングリストTwitter