Java言語仕様の15.8.3節「this
」 には、以下のように記載されている[JLS 2015]。
一次式として用いられた場合、
this
キーワードはインスタンスメソッドの起動(§15.12)が行われたオブジェクト、または構築中のオブジェクトへの参照値を表す....
this
の型は、this
キーワードが記述されているクラスまたはインターフェイス型T
である....実行時に実際のオブジェクトが参照するクラスは、
T
(T
がクラス型である場合) あるいはT
のサブクラスとなる。
現在のスコープを越えて this
参照がアクセス可能な状態になることを逸出(escape)と呼ぶ。よくある this
参照の逸出例としては、以下のようなものがある。
- private ではなくオーバーライド可能なメソッドが、オブジェクトの構築時にクラスのコンストラクタから呼び出され、
this
を返す。(詳細は「MET05-J. コンストラクタにおいてオーバーライド可能なメソッドを呼び出さない」を参照。) - 可変クラスの private ではないメソッドから
this
を返す。これを利用して、呼出し元はオブジェクトの状態を間接的に操作できる。これは、通常メソッドチェーンの実装で生じる。詳細は「VNA04-J. メソッドチェーン呼出しのアトミック性を確保する」を参照。 - オブジェクトの構築時にクラスのコンストラクタから呼び出される「よそ者メソッド(alien method)」に、引数として
this
を渡す。 - 内部クラスの使用。内部クラスは static 宣言されない限り、その外部クラスのインスタンスへの参照を保持している。
- オブジェクトの構築時にコンストラクタが、
this
参照を public static 宣言された変数に代入することにより公開する。 - オブジェクトの構築時にコンストラクタが例外をスローする。このようなコードはファイナライザ攻撃を受ける危険がある。詳しくは「OBJ11-J. コンストラクタが例外をスローする場合には細心の注意を払う」を参照。
- 内部オブジェクトの状態を、「よそ者メソッド」へ渡す。結果的に、よそ者メソッドが内部メンバであるオブジェクトの
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)