2012年04月18日

不変クラスについてあれこれ考える


"不変クラス"とはその名の通り、状態が生成時から決して変化しないクラスをいいます。不変クラスの代表格はStringですが、Stringクラスは内部の状態を変更できる術を外部に一切公開していません。

あるインスタンスが生成された時点の状態から絶対に変化しないという確証がプログラミングを簡略化することを想像するのは難しいことではありません。不変クラスは確実にスレッドセーフです。どのスレッドから共有されてもなんの問題も引き起こしません。また、オブジェクトの属性にちなんだ変数名をつけても問題なく、ソースが分かりやすくなります。しかし、オブジェクトを変化させないというルールをAPI使用者のコーディングや規約のみによって保とうとするのはまず無理でしょう。業務アプリケーションは大人数でコーディングを行いますので、完全に強制できる仕組みがない以上、規約はせいぜい気休め程度でしかありません。

今日は不変クラスについて勉強したいと思います。不変クラスはどのような条件を満たす必要があるのか、どう使用していけばいいのかを考えていきたいと思います。

不変クラスの条件

手元にある『Effective Java 第2版』によると不変クラスは以下の条件を満たす必要があるとされています。

@オブジェクトの状態を変更するためのいかなるメソッドも提供しない。
Aクラスが拡張できないことを保証する。
Bすべてのフィールドをfinalにする。
Cすべてのフィールドをprivateにする。
D可変コンポーネントに対する独占的アクセスを保証する。

@は単にsetterにあたる物や、メンバ変数の状態を変更できる公開メソッドを作成してはいけませんよ。ということです。Aはクラスがextendsによって拡張できないことを保証すればいいわけですから、クラスをfinalするのがもっとも簡単です。その他の方法としては、コンストラクタをprivateにしてstaticファクトリメソッドを提供するという方法があります。B〜Cはそのままの意味ですね。分かりにくいのがDですが、これはサンプルを見ながら確認していきます。まずは、@〜Cだけを考えて不変クラスを作成していきます。

不変クラスの悪例
以下に示すのは不完全な不変クラスです。悪い例なので注意。
/**
 * 不完全な不変クラス
 * @author ragtimer
 */
public class Person {
	/**
	 * 名前.
	 */
	private final String name;
	/**
	 * 体重.
	 */
	private final double weight;
	/**
	 * 身長.
	 */
	private final double height;
	/**
	 * 誕生日.
	 */
	private final Date birth;
	/**
	 * 友達.
	 */
	private final List<Person> friends;

	/**
	 * ビルダー
	 */
	public static class Builder{

		//必須パラメータ
		/**
		 * 名前.
		 */
		private final String name;
		/**
		 * 体重.
		 */
		private final double weight;
		/**
		 * 身長.
		 */
		private final double height;
		/**
		 * 誕生日.
		 */
		private final Date birth;

		//オプションパラメータ
		/**
		 * 友達.
		 */
		private List<Person> friends = null;

		/**
		 * 必須項目を作成するコンストラクタ.
		 */
		public Builder(String name,double weight,double height,Date birth){
			this.name = name;
			this.weight = weight;
			this.height = height;
			this.birth = birth;
		}
		/**
		 * オプション項目(友達)を設定する.
		 * @param friends
		 * @return
		 */
		public Builder friends(List<Person> friends){
			this.friends = friends;
			return this;
		}

		/**
		 * ビルドメソッド.
		 * @return
		 */
		public Person build(){
			return new Person(this);
		}
	}
	/**
	 * privateコンストラクタ
	 * @param builder
	 */
	private Person(Builder builder){
		this.name = builder.name;
		this.weight = builder.weight;
		this.height = builder.height;
		this.birth = builder.birth;
		this.friends = builder.friends;
	}
	/**
	 * 友達表示.
	 */
	public void printFriends(){
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy'年'MM'月'dd'日'");
		if(this.friends != null){
			for(Person friend : friends){
				System.out.println(friend.name + "!" + sdf.format(friend.birth).toString() + "生まれ!");
			}
			System.out.println(this.name + "!" + sdf.format(this.birth).toString() + "生まれ!");
			System.out.println("超平和バスターズはずっとなかよし!!");
			System.out.println();
		}
	}

	//以下アクセサー
	public String getName() {
		return name;
	}
	public double getWeight() {
		return weight;
	}
	public double getHeight() {
		return height;
	}
	public Date getBirth() {
		return birth;
	}
	public List<Person> getFriends() {
		return friends;
	}
}
ちょっと引数が多いのでBuilderパターンを使いました。最初に言っておくと、これは中途半端な不変クラスです。前述した不変クラスの条件のうち@〜Cは満たしていますが、Dの「可変コンポーネントに対する独占的アクセスを保証する」を満たしていません(このクラスはfinalではないですがAは満たしてます。継承先から呼び出し可能なコンストラクタがありませんので、子クラスを実装できないからです)。ではどうDを満たしていないのか。クライアントコードを書いて攻撃してみます。
public static void main(String[] args) throws ParseException {
	//超平和バスターズを作成
	Person jintan
		= new Person.Builder("じんたん", 55.0, 165.0, toDate("1995/9/18")).build();
	Person anaru
		= new Person.Builder("あなる", 49.0, 164.0, toDate("1995/11/18")).build();
	Person yukiatsu
		= new Person.Builder("ゆきあつ", 68.0, 181.0, toDate("1995/7/20")).build();
	Person tsuruko
		= new Person.Builder("つるこ", 47.0, 170.0, toDate("1995/5/10")).build();
	Person poppo
		= new Person.Builder("ぽっぽ", 89.0, 185.0, toDate("1995/1/16")).build();

	List<Person> chouHeiwaBusters = new ArrayList<Person>();
	Collections.addAll(chouHeiwaBusters, jintan,anaru,yukiatsu,tsuruko,poppo);

	//みんなをメンマの友達に
	Person menma
	= new Person.Builder("めんま", 36.0, 147.0, toDate("1995/1/16"))
											.friends(chouHeiwaBusters).build();

	//友達表示
	menma.printFriends();

	//----------------------//
	//悪意あるragtimerの攻撃//
	//----------------------//
	Person ragtimer
		= new Person.Builder("らぐたいまあ", 74.0, 181.0, toDate("1981/8/18")).build();
	//みんないなくなれ
	chouHeiwaBusters.clear();

	//超平和バスターズの一員に
	menma.getFriends().add(ragtimer);

	//めんまとふたりっきり・・・
	menma.printFriends();
}
private static Date toDate(String str) throws ParseException{
	Date date = DateFormat.getDateInstance().parse(str);
	return date;
}
実行結果はこちら。↓↓↓
じんたん!1995年09月18日生まれ!
あなる!1995年11月18日生まれ!
ゆきあつ!1995年07月20日生まれ!
つるこ!1995年05月10日生まれ!
ぽっぽ!1995年01月16日生まれ!
めんま!1995年01月16日生まれ!
超平和バスターズはずっとなかよし!!

らぐたいまあ!1981年08月18日生まれ!
めんま!1995年01月16日生まれ!
超平和バスターズはずっとなかよし!!

見事に中身を変えられてますね。要するに、不変クラス(にしたいクラス)が持っている可変コンポーネントの参照をクライアントが操作できることが問題なわけです。問題になるのはbirthメンバとfiriendsメンバ、つまりDateとListですね。この2つは可変クラスですから、内部メンバの参照をクライアントが操作できるのが好ましくないわけです。これがDのいう「可変コンポーネントに対する独占的アクセス」を崩されている状態ですね。

ソース上の問題は2箇所あります。ひとつめは可変コンポーネントを設定しているところ、つまりコンストラクタとBuilderの設定メソッドです。参照をそのまま自フィールドに入れています。これは、クライアントが後で渡したインスタンスに対して変更を加えると、作成したオブジェクトにまで影響が及ぶということを意味しています。上記のmain()の中で、chouHeiwaBusters変数をclear()している攻撃が有効になります。

もうひとつはアクセサーです。可変オブジェクトの参照をそのまま返していますので、同様の問題が発生します。menma.getFriends()でfriendsを呼び出し、それにadd()してるのがその問題点をついてます。ではこれらの問題点を解消していきます。

防御的にコピーする
この問題を解消するには「防御的コピー」という方法を使用します。それぞれ、以下のように修正します。
・値を設定している箇所(Date)
public Builder(String name,double weight,double height,Date birth){
	this.name = name;
	this.weight = weight;
	this.height = height;
	this.birth = new Date(birth.getTime());		//防御的にコピー
}
・値を設定している箇所(List)
public Builder friends(List<Person> friends){
	//防御的にコピー
	List<Person> copyList = new ArrayList<Person>();
	copyList.addAll(friends);
	this.friends = copyList;
	return this;
}
・アクセサー(Date)
public Date getBirth() {
	//防御的にコピー(clone()を使用してもOK)
	return new Date(birth.getTime());
}
・アクセサー(List)
public List<Person> getFriends() {
	//防御的にコピー(clone()を使用してもOK)
	List<Person> copyList = new ArrayList<Person>();
	copyList.addAll(friends);
	return copyList;
}
値を設定するときもアクセサーから参照を返す時も、毎回別のオブジェクトを作成しています。つまりクライアントはいくら設定した変数やアクセサから取得した変数に変更を加えても、このクラスの内部構造には何の影響もありません。これで完璧な不変クラスになりました。ただし、ひとつ注意があって、値を内部構造に設定する際はclone()を使用してはいけないということです。クライアントから引き渡されたクラスが悪意あるDate/Listのサブクラスであり、clone()が攻撃者によってオーバライドされていてもPersonクラスでは感知できないからです。設定する箇所さえキチンとコピーすれば、アクセサーはclone()でかまいません。なぜならそのクラスはjava.util.Date、あるいはjava.util.ArrayListに確定するからです。では実際にclone()を使用した場合の攻撃をみてみます。

内部構造設定時にclone()を使うと・・・
例えば先ほどのDateを誤ってこう修正したとします。
public Builder(String name,double weight,double height,Date birth){
	this.name = name;
	this.weight = weight;
	this.height = height;
	this.birth = (Date)birth.clone();		//誤ってclone()を使用
}
コンストラクタでclone()を使ってますね。では悪意を持ってクライアントコードを書きます。まずは邪悪なDateから
/**
 * 邪悪なDate
 * @author ragtimer
 *
 */
public class EvilDate extends Date{
	public static List<Date> EVIL_DATE_LIST = new ArrayList<Date>();
	/**
	 * 悪意あるcloneの実装
	 */
	@Override
	public Date clone(){
		Date date = (Date) super.clone();
		EVIL_DATE_LIST.add(date);
		return date;
	}
}
clone()を実装して、内部のstatic領域に作成した参照を隠し持っていますね。続いてはmainです。
	public static void main(String[] args) throws ParseException {
		//超平和バスターズを作成
		Person jintan
			= new Person.Builder("じんたん", 55.0, 165.0, toDate("1995/9/18")).build();
		Person anaru
			= new Person.Builder("あなる", 49.0, 164.0, toDate("1995/11/18")).build();
		Person yukiatsu
			= new Person.Builder("ゆきあつ", 68.0, 181.0, toDate("1995/7/20")).build();
		Person tsuruko
			= new Person.Builder("つるこ", 47.0, 170.0, toDate("1995/5/10")).build();
		Person poppo
			= new Person.Builder("ぽっぽ", 89.0, 185.0, toDate("1995/1/16")).build();

		List chouHeiwaBusters = new ArrayList();
		Collections.addAll(chouHeiwaBusters, jintan,anaru,yukiatsu,tsuruko,poppo);

		//みんなをメンマの友達に
		Person menma
		= new Person.Builder("めんま", 36.0, 147.0, toDate("1995/1/16"))
												.friends(chouHeiwaBusters).build();
		//友達表示
		menma.printFriends();
		
		//----------------------//
		//悪意あるragtimerの攻撃//
		//----------------------//
		for(Date date : EvilDate.EVIL_DATE_LIST){
			//全員を1920年生まれのジジババに!
			date.setYear(20);
		}

		//あの頃は楽しかったねえ・・フガフガ
		menma.printFriends();
	}
	private static Date toDate(String str) throws ParseException{
		Date date = DateFormat.getDateInstance().parse(str);
		EvilDate eDate = new EvilDate();
		eDate.setYear(date.getYear());
		eDate.setMonth(date.getMonth());
		eDate.setDate(date.getDate());

		return eDate;
	}
}
実行結果はこちら↓↓↓
じんたん!1995年09月18日生まれ!
あなる!1995年11月18日生まれ!
ゆきあつ!1995年07月20日生まれ!
つるこ!1995年05月10日生まれ!
ぽっぽ!1995年01月16日生まれ!
めんま!1995年01月16日生まれ!
超平和バスターズはずっとなかよし!!

じんたん!1920年09月18日生まれ!
あなる!1920年11月18日生まれ!
ゆきあつ!1920年07月20日生まれ!
つるこ!1920年05月10日生まれ!
ぽっぽ!1920年01月16日生まれ!
めんま!1920年01月16日生まれ!
超平和バスターズはずっとなかよし!!
みんな爺さん婆さんになってしまいました。これが内部構造を設定する際にclone()を使用する危険性です。

不変クラス/防御的コピーの使いどころ
不変クラスはどういった時に使えばいいでしょうか?『Effective Java 第2版』では「正当な理由がない限り、クラスは全て不変にすべきである」と書いてあります。確かに、でき得る限り不変にすればバグの確率は下がりますね。ただ、今私は悪意をもってクラスの不変性を崩そうと外部から攻撃していましたが、ほとんどの場合実装者に悪意などありません。ありませんが、意味がわからずAPIを修正されて不変クラスが崩される可能性もありますし、先ほどのアクセサーを通して内部構造が取得できていると信じ込み、それらに対して変更を加えることで仕様を満たしたと満足される可能性もあります。従って、Javadocや然るべきドキュメントに不変であることを明記しておいたほうがいいかもしれません。今回は便宜上DateやListを使用していますが、日付の情報を持つならばDate.getTime()で取得されるlongを持ったほうが健全ですし、「友達」といった明らかに可変な概念を不変クラスの内部構造にすべきなのかという問題はもちろんあります。これはあくまでサンプルです。

防御的コピーに関しては、アクセサーに要求されるたびに毎回オブジェクトが生成されるという性能的リスクがあるのは確かです。しかし、不変クラスは状態が固定されているため、オブジェクトの再利用がされやすいという性能的メリットもあります。どちらを優先するかは状況しだいでしょうが、防御的コピーそのものは不変クラスに限らず重要です。例えば、このPersonクラスに友達を追加するadd(Person)メソッドを追加したとします。その瞬間、このクラスは不変ではありませんが、それを理由に防御的コピーをやめるべきではないでしょう。なぜなら、「内部構造が変更できるのはadd(Person)からのみである」という保証は品質やデバッグの観点から重要であることには変わりないからです。

今日はこのへんで。なおこの記事は『Effective Java 第2版』を大いに参考にしています。凄くためになる本なのでよかったらどうぞ。あとメンマ大好き。ペロペロしたい(^ω^)
posted by sandman at 21:57| Comment(1) | Java | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
たくさんイヤラシイ事をおねだりできるよ☆-(ゝω・ )ノ http://sns.44m4.net/
Posted by 久美子 at 2012年05月24日 07:49
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
×

この広告は1年以上新しい記事の投稿がないブログに表示されております。