すべての get メソッドに対して set メソッドを書きたい衝動に抵抗してください。可変にすべきかなり正当な理由がない限り、クラスは不変であるべきです。
不変性が実用的でないクラスもあります。もし、クラスを不変にできないのであれば、その可変性をできるだけ制限すべきです。オブジェクトが存在できる状態の数を減らすことで、オブジェクトについて明確に論理的に考えるのが容易となり、エラーの可能性も減ります。したがって、final にできないやむを得ない理由がない限り、すべてのフィールドを final にしてください。
『Effective Java 第2版』
項目15 可変性を最小限にする
こんにちわ。ちかです
Scalaのimmutableなコレクションについてという記事を読んで
改めて可変性/不変性について考えています。
不変オブジェクトと再代入 (再束縛) 不可能な変数
不変と言われてもピンとこないかもしれません。
注意してほしいのは、不変オブジェクトと再代入不可能な変数の違いです。
(変数への再代入も最小限にすべきであることに違いはありませんが。)
Java で言うと、、、
- String は不変クラスです。
String のインスタンスは不変オブジェクトです。 - StringBuilder は可変クラスです。
StringBuilder のインスタンスは可変オブジェクトです。 - final 修飾子つきの変数は再代入不可能です。
再代入不可能な変数が可変オブジェクトを参照することもあれば、
再代入可能な変数が不変オブジェクトを参照することもあります。 - あるオブジェクトのフィールド (変数) への再代入は、そのオブジェクトの変更です。
あるオブジェクトのフィールド (オブジェクト) の変更は、そのオブジェクトの変更です。
初期化済みのオブジェクトに対して変更する手段がないとき、そのオブジェクトは不変と言えます。
public final class Immutability { public static void main(String[] args) { { StringBuilder stringBuilder = new StringBuilder("mutable"); final StringBuilder finalStringBuilder = stringBuilder; // ※ StringBuilder の一部のインスタンスメソッドはオブジェクトを変更する finalStringBuilder.setLength(0); // オブジェクトの変更 finalStringBuilder.append("muted"); // オブジェクトの変更 System.out.println(stringBuilder); // "muted" System.out.println(finalStringBuilder); // "muted" // ※ 変数への再代入はオブジェクトを変更しない stringBuilder = new StringBuilder("reassigned"); // 変数への再代入 System.out.println(stringBuilder); // "reassigned" System.out.println(finalStringBuilder); // "muted" // ※ final 修飾子つきの変数への再代入はコンパイルエラーを起こす finalStringBuilder = new StringBuilder(); // コンパイルエラー } { String string = "immutable"; final String finalString = string; // ※ 変数への再代入はオブジェクトを変更しない string = "reassigned"; // 変数への再代入 System.out.println(string); // "reassigned" System.out.println(finalString); // "immutable" // ※ String のインスタンスメソッドはいずれもオブジェクトを変更しない // (substring() などは新たな String オブジェクトを生成する) string += finalString.substring(5); // 変数への再代入 System.out.println(string); // "reassignedable" System.out.println(finalString); // "immutable" // ※ final 修飾子つきの変数への再代入はコンパイルエラーを起こす finalString = "reassigned"; // コンパイルエラー } } }
不変性がもたらすメリット (可変性の問題点)
IBM の Javaの理論と実践: 可変性か、不変性か?というアーティクルに、不変性のメリットがいくつも紹介されています。(ぶっちゃけると、私の今回の記事は上記アーティクルの劣化コピーと言えます(苦笑))
紹介されているメリットのうち、コード例があって直感的に分かりやすい部分を以下に引用します。
(スレッドセーフ性など他のメリットは上記リンク先を参照してください。)
オブジェクトが可変の場合は、そのオブジェクトへの参照を保存するときに、いくつか気をつけなければならない点があります。リスト1のコードを見てください。ここではスケジューラーで実行する2つのタスクをキューに入れています。このコードは、最初のタスクを今実行し、2番目のタスクを次の日に実行することを目的としています。
リスト1. 可変であるDateオブジェクトの潜在的な問題点
Date d = new Date(); scheduler.scheduleTask(task1, d); d.setTime(d.getTime() + ONE_DAY); scheduler.scheduleTask(task2, d);
Date は可変であるため、scheduleTask メソッドでは、十分な注意を払って (clone() などを使用して) 防御的に日付パラメーターを内部データ構造にコピーする必要があります。そうでないと、task1 と task2 は両方とも明日実行される可能性があり、それでは予定の動作と異なります。ひどい場合には、タスク・スケジューラーが使用する内部データ構造が破損する可能性もあります。scheduleTask() のようなメソッドを記述する場合には、日付パラメーターを防御的にコピーすることを忘れがちです。これを忘れると、しばらくの間は表面化しないものの、いざ起こってみると追跡に長い時間のかかるわかりにくいバグを作ることになります。Date クラスが不変であれば、この種のバグが発生することはないでしょう。
防御的コピー
上に引用したとおり、可変オブジェクトを扱う場合には注意が必要です。
可変オブジェクトを外部から変更されないよう安全に保持するには、防御的コピーというイディオムを使います。
(上の引用にも「防御的にコピーする」的な言い回しが出てきますが、このことです。)
public final class DateOwner { private final Date date; public DateOwner(Date date) { // 防御的コピー: // 引数として渡された可変オブジェクトはそのまま保持せず、コピーを保持する // (渡された後で変更されても this.date に影響しないように) this.date = date.clone(); } public Date getDate() { // 防御的コピー: // 保持している可変オブジェクトはそのまま返さず、コピーを返す // (dateOwner.getDate().setXX() のような変更が this.date に影響しないように) return date.clone(); } }
保持するオブジェクトのデータ型によって防御的コピーの生成方法は変わってきます。
(clone(), コピーコンストラクタ, etc)
可変オブジェクトをカプセル化する際には、防御的コピーは必須といえるでしょう。
ですが、人間は、間違いなく、忘れます。
不変オブジェクトを扱うときは、引数をそのまま保持しても、保持しているオブジェクトをそのまま返しても、内部状態を変更・破壊される心配はありません。
不変クラスのレシピ
Java で不変クラスを作成するには、次の5つの原則に従います。(『Effective Java 第2版』項目15より)
- オブジェクトの状態を変更するためのいかなるメソッドも提供しない
- クラスが拡張できないことを保証する
(クラスを final にする。もしくはコンストラクタを private にして static ファクトリメソッドを提供する。) - すべてのフィールドを final にする
- すべてのフィールドを private にする
- 可変コンポーネントに対する独占的アクセスを保証する
(可変オブジェクトを外部からそのまま受け取って保持したり、外部へそのまま渡したりしない。必要なら防御的コピーを行う。)
String クラスの実装と
AbstractStringBuilder クラス(StringBuilder クラス の実装補助基底クラス) を
見比べてみるとおもしろいかもしれません。
さいごに
C/C++ では、変数 (関数の引数や戻り値も含む) やメンバー関数に const をつけることで、
オブジェクトの不変性をコンパイラにチェックさせることができます。
Java には C/C++ の const に相当する機能がない (final では不変性をチェックできない) ため、
人間がオブジェクトの変更に注意してコーディングする必要がある、ということです。
(C/C++ では人間が注意して const をつけていくわけですが、「const により、他のプログラマにもコンパイラにも、指定したオブジェクトの値を変更しないよう知らせることができる」のは大きな大きな利点なのです。
『Effective C++ 第3版』3項「可能ならいつでもconstを使おう」参照。)
ということで
この言葉を贈って終わりにします。
人は「変わらざる中心」がなければ、変化に耐えることができない。
スティーブン・R・コヴィー
ついでに
Scala の不変リスト scala.collection.immutable.List のインスタンスは、初期化後に要素の追加・削除・再代入 (再束縛) はできませんが、要素を取得して変更することはできます。
scala> val list = List(java.util.Calendar.getInstance()) list: List[java.util.Calendar] = List(java.util.GregorianCalendar[time=1371644777333,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Asia/Tokyo",offset=32400000,dstSavings=0,useDaylight=false,transitions=10,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2013,MONTH=5,WEEK_OF_YEAR=25,WEEK_OF_MONTH=4,DAY_OF_MONTH=19,DAY_OF_YEAR=170,DAY_OF_WEEK=4,DAY_OF_WEEK_IN_MONTH=3,AM_PM=1,HOUR=9,HOUR_OF_DAY=21,MINUTE=26,SECOND=17,MILLISECOND=333,ZONE_OFFSET=32400000,DST_OFFSET=0]) scala> list(0).getTime() res0: java.util.Date = Wed Jun 19 21:26:17 JST 2013 scala> list(0).add(java.util.Calendar.YEAR, 10) scala> list(0).getTime() res2: java.util.Date = Mon Jun 19 21:26:17 JST 2023
したがって、要素のデータ型が不変であれば不変に、要素のデータ型が可変であれば可変になります。
(防御的コピーをするためにはコピーの生成方法を知る必要があるので、制約のないジェネリッククラスのインスタンスは防御的コピーができないのかもしれませんね。。)