前回は概念だけの振りでしたが、今回は実装も合わせて具体例を出していきたいと思います。
今回のテーマは不変オブジェクトです。
今回のターゲット
Javaにおける参照はC言語におけるポインタに毛の生えたようなものです。参照は対象となるオブジェクトを指し示しているだけなので、オブジェクトの保持する値を変更すると参照元全てからその変更が見えてしまいます。
これはC言語時代から変わらないプログラミングのトピックです。プログラミングのテクニックとしてこれをうまく使えば便利ではあるのですが、うっかりすると、コピーをいじるつもりが原本を書き換えてしまって大変なことに!ということがおこります。(書類を例にした比喩ですが、あまりうまい比喩ではないですね…)
この由緒あるバグは、参照・ポインタの挙動をうまく把握できていない初心者によって多く引き起こされますが、慣れている上級者でも、うっかりしているとやってしまうものです。
バグパターン検出ツールfindbugsでもこの問題を検出することが出来ます。 Naoki氏のページ「Program Island」の FindBugsパターン説明 での解説が日本語で分かりやすいかと思います。
書籍では 標準FindBugs完全解説 がわかりやすいでしょうか。
今回のターゲットとなるバグパターンは MS: Public static method may expose internal representation by returning array や、これに類するものとなります。
問題となるコード
サンプルとして以下のようなコードを用意しました。 ExposeInternalクラスはフィールドにjava.awt.Pointオブジェクトを持っているだけのクラスです。 getterが用意してあり、内部に保有するPointオブジェクトを参照することが出来ます。
import java.awt.Point;
public class ExposeInternal {
/** privateなフィールド。外から変更されたくない */
private Point point;
/**
* コンストラクタ。初期値となるPointオブジェクトを受け取り保管
* @param point 初期ととなるPointオブジェクト
*/
public ExposeInternal(Point point) {
this.point = point;
}
/**
* pointのgetterメソッド
* @return pointの参照をそのままreturn
*/
public Point getPoint() {
return this.point;
}
/**
* 内部状態を確認しやすいようにObject.toString()をオーバーライド
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "ExposeInternal : (" + this.point.x + ", " + this.point.y + ")";
}
public static void main(String[] args) {
ExposeInternal test = new ExposeInternal(new Point(32, 16));
System.out.println(test); // (1)
Point p = test.getPoint();
p.x = 0; // (2)
System.out.println(test); // (3)
}
}
このコードの実行結果は以下のようになります。
ExposeInternal : (32, 16)
ExposeInternal : (0, 16)
(1)の時点では初期値そのままですが、(2)で内部情報であるPointオブジェクトを受け取り、そのオブジェクトのフィールドを直接更新してしまいました。
(2)での変数pの参照が指し示すオブジェクトと、ExposeInternalオブジェクトtestのフィールドpointの参照が指し示すオブジェクトが同一であるためにこのような現象が起きます。
(3)の結果で分かるように、testオブジェクトの外から内部状態を変えることができてしまうわけですね。
このようにオブジェクトの内部に保持するオブジェクトを外部に安易に返すと、この現象に起因するバグの元となります。
Javaの標準APIに学ぶ解決策
解決策はいくつかありますが、簡単なのは内部で保有するPointオブジェクトをそのまま返すのではなく、そのオブジェクトのコピーを作って返すという方法です。
/**
* pointのgetterメソッド
* @return pointの参照のコピーを作ってreturn
*/
public Point getPoint() {
return new Point(this.point);
}
今回のサンプルではPointオブジェクトですからこれで十分です。しかし、例えばjava.util.Listといったコレクションではどうでしょう?コピーを作るためのコストが非常に大きくなってしまいます。
ここで、アプローチを「変更されても平気」から「誤って変更した場合に例外を起こして警告し早期発見に努める」に切り替えましょう。
Listの場合は標準APIに便利なメソッドが用意されています。 java.util.Collections#unmodifiableList()です。
このメソッドにListを渡すと対象のListに対して変更操作をしようとした場合にUnsupportedOperationExceptionを throwするラッパーオブジェクトを返してくれます。
GoFデザインパターンでいうProxyパターンになりますね。
/**
* 内部情報Listのgetterメソッド。
* ただし、変更不可能となっており、変更操作に対してはUnsupportedOperationException.
* @return 内部情報List.
*/
public List getList() {
return Collections.unmodifiableList(this.list);
}
こうすることで、うっかり取得したListに変更操作を加えた場合に発生するバグを早期に発見することができます。(なお、上記例ではString型を格納したListですから問題ないですが、Listに格納されるオブジェクトがPointなどの変更可能なフィールドを持つ場合はやはりそのオブジェクトを変えると問題が起きます…。)
Listの例にならい、独自の型でもラッパー型を用意することで予期せぬ変更に対しての防衛となります。
Immutableオブジェクトの安全性
Immutableオブジェクトという言葉を聞いたことあるでしょうか?日本語では不変オブジェクトと呼ばれます。
初期値を得た後、内部情報が変更されないオブジェクトです。
有名なところではjava.lang.Stringや、java.lang.Integerといったものがあります。これらは初期値をとった後、内部の情報が変更されることはありません。 Stringを変更しているように見えるのは、新しいStringオブジェクトを作り直しているのです。
このような不変オブジェクトでは、同一のオブジェクトを参照している場合に、不意に他の箇所から値が変更される心配をする必要がありません。マルチスレッド下でも非常にシンプルな使い方ができます。
特別な理由がないのであれば、オブジェクトはできるだけ不変になるように設計しておくとこれらのバグとの遭遇を減らすことが出来ます。
投稿日時 : 2007年8月17日 18:18