不変オブジェクトの抜け道
クラスの製作者は常に「クライアントは不変式を破壊するためにあらゆる方法を使ってくる」と仮定し、防御的にプログラムを書くべきです。 「セキュリティホールをついてくるような悪意のある開発者」から「単純な間違いをおかす初心者」まで、様々なユーザーがクラスを利用するからです。
オブジェクトの内部状態を変えるためには、一般的にそのクラスのメソッドを経由する必要があります。 しかし、意図せずにそれら以外の抜け道を与えている場合が非常に多いです。
例えば 2 つの Date インスタンスを受け取って、期間を表すクラス Period を考えてみます。 このクラスでは「開始は終わりよりも前である」という不変式を常に満たす必要があります。
// // 不完全な不変クラス // public class Period { private final Date start; private final Date end; /** * @param start 期間の開始 * @param end 期間の終わり。開始より前であってはならない。 * @throws IllegalArgumentException start が end の後の場合。 * @throws NullPointerException start か end が null の場合 */ public Period(Date start, Date end) { if (start.compareTo(end) > 0) { throw new IllegalArgumentException(start + " after " + end); } this.start = start; this.end = end; } public Date getStart() { return start; } public Date getEnd() { return end; } }
このクラスは2つのメンバ変数が final なので不変クラスに見えます。 しかし、実際にはそうではありません。
コンストラクタに渡したオブジェクトの中身は可変です。 クライアントはこれを利用すると以下のように不変式を破ることができます。
Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); end.setYear(78);
コンストラクタでの防御的コピー
このような攻撃を防ぐためにはコンストラクタ内で防御的コピーを行う必要があります。
public Period(Date start, Date end) { // コピーを作成してそれを保持。 this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if (this.start.compareTo(this.end) > 0) { throw new IllegalArgumentException(start + " after " + end); } }
上記の防御的なコピーには2つのポイントがあります。
1つ目は、パラメータの正当性検査が防御的コピーのあとに行われている点です。 防御的コピーの前にパラメータの正当性検査をした場合、検査のあと、実際にインスタンスがコピーされて値が代入されるまでの間にわずかな隙間が生じます。 この隙間を狙って他のスレッドがパラメータを変更する可能性があります。
これは Time of Check/Time of Use 攻撃として知られる有名な攻撃方法の一つです。 この攻撃を防ぐためにも、防御的コピーのあとで正当性検査をする必要があります
2つ目は clone() を利用しない点です。 Date は final ではありませんから、clone メソッドがオーバーライドされたクラスのインスタンスが渡されるかもしれません。 もしかしたら、コピーしたと思わせて自分自身を返すだけのような clone() が実装されているかもしれません。
public class AttackDate extends Date { @Override public AttackDate clone() { return this; } }
getter での防御的コピー
コンストラクタで防御的コピーをするように変更しましたが、これでもまだ完全ではありません。
現在の実装では getter は内部インスタンスへの参照を直接返しています。 そのため、getter を通じて返されたインスタンスに変更を加えると不変式を破ることができます。
Date start = new Date(); Date end = new Date(); Period p = new Period(start, end); p.end().setYear(78);
これを防ぐ方法は簡単です。 内部フィールドを返すときには防御的コピーをすることです。
public Date start() { return new Date(star.getTime()); } public Date end() { return new Date(end.getTime()); }
ここでは clone() を用いることが許されます。 なぜなら、コンストラクタの中で Date インスタンスが生成されていることが保証されるからです。
本当の教訓
防御的コピーについて話してきましたが、本当の教訓は『可能な限りオブジェクトの構成要素として不変オブジェクトを使うべき』ということです。 この場合、コンストラクト時に検査をしておけば後から不変式を破られることはありません。
たとえば Date の場合、Date.getTime() で取得できる基本データ型の long を使用しておけば防御的コピーの心配をする必要はなくなります。
防御的コピーはパフォーマンス上のペナルティがあるため、常に正当化されるとは限りません。 そのような場合は適切にドキュメンテーションし、クライアントによるインスタンスの変更は動作保証外であることを明記しておきましょう。