前回の記事で、Javaにおける不変オブジェクトのメリットを簡単に説明しました。
前回はStringやjava.timeパッケージ内の時間クラスなど、JavaのAPIとして用意されているクラスを使って説明しましたが、今度は自作クラスを不変オブジェクトにするための工夫について説明してみます。
参照の隠蔽
まず大事なのは「参照を隠す」ということです。
言葉にすると難しそうですが、要はインスタンス変数をprivateにして、不必要なsetterは定義しないようにしましょうね、ということです。
以下は、 Person.java
という、参照の隠蔽を実践したクラスです。
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
このPersonクラスを使うメインクラスは ImmutableSample.java
とします。
public class ImmutableTest {
public static void main(String args[]) {
String name = "Taro";
Person taro = new Person(name);
}
}
Personクラスの使用者であるImmutableTestクラスは、Personクラスの name
フィールドにアクセスする術を持たず、またname変数もStringクラスがそもそも不変であるため、taro
インスタンスの保持する値( name
変数に格納された "Taro"
という文字列)を変更することはできません。
インスタンス変数がString型だけであれば、これで不変クラスは完成です。
防衛的コピー
では次に、先ほどのPersonクラスに「誕生日」を表すDate型の birthday
変数を追加してみます。
public class Person {
private String name;
private Date birthday;
public Person(String name, Date birthday) {
this.name = name;
this.birthday = birthday;
}
public String toString() {
return String.format("%sさんの誕生日は%sです。", name, new SimpleDateFormat("yyyy/MM/dd").format(birthday));
}
}
先ほどの「参照の隠蔽」で説明した通り、このクラスはインスタンス変数が全て private
でsetterメソッドも定義されていません。しかし、このクラスは不変ではありません。以下のように使うことで、birthday
を書き換えることができるためです。
public class ImmutableTest {
public static void main(String args[]) throws ParseException {
// Taroインスタンスの生成
String name = "Taro";
Date birthday = new SimpleDateFormat("yyyyMMdd").parse("19870929");
Person taro = new Person(name, birthday);
System.out.println(taro.toString());
// birthdayを外から変更
birthday.setTime(System.currentTimeMillis());
System.out.println(taro.toString());
}
}
Taroさんの誕生日は1987/09/29です。
Taroさんの誕生日は2016/09/29です。
これでは、mainメソッドの birthday
変数が持つ参照値があとの処理に引き回されでもしたら、いつTaroの誕生日が変わってしまうか予測できない状況になってしまいます。
これを防ぐための工夫が「防衛的コピー」です。Personクラスを以下のように書き換えます。
public class Person {
private String name;
private Date birthday;
public Person(String name, Date birthday) {
this.name = name;
this.birthday = new Date(birthday.getTime()); // birthdayインスタンスのコピーを保持する
}
public String toString() {
return String.format("%sさんの誕生日は%sです。", name, new SimpleDateFormat("yyyy/MM/dd").format(birthday));
}
}
この状態で、ImmutableTest.javaのソースコードはそのまま変えずに実行すると、
Taroさんの誕生日は1987/09/29です。
Taroさんの誕生日は1987/09/29です。
となり、いくら外からTaroの持つ変数を書き換えようとしてもできない状態を作り出すことができます。
また多くの場合、Taroの誕生日を参照するためのgetterメソッドを用意する必要がります。このとき単純にgetterメソッドを用意してしまうと、せっかくコンストラクタでコピーした birthday
の参照値を使用者に渡してしまうことになります。
public Date getBirthday() {
return birthday;
}
このような場合も防衛的コピーを使います。
public Date getBirthday() {
return new Date(birthday.getTime());
}
これで、不変でない Date
型のフィールドを持つPerson
クラスを不変にすることができました。
注)
日付クラスについてはJava8から不変性をもつクラスがいくつも追加されていますので、実際は「そもそもDateクラスを使わない」というのが正しいのですが、今回は説明のためにあえてDateクラスを使用しています。
リストを持つクラスを不変にする
最後に、Personに休暇日を表す vacations
を追加してみます。
public class Person {
private String name;
private Date birthday;
private List<Date> vacations;
public Person(String name, Date birthday, List<Date> vacations) {
this.name = name;
this.birthday = new Date(birthday.getTime());
this.vacations = new ArrayList<>(vacations);
}
public List<Date> getVacations() {
return new ArrayList<>(vacations);
}
}
birthday
と同じように防衛的コピーを使ってみました。これで大丈夫でしょうか。
public class ImmutableTest {
public static void main(String args[]) throws ParseException {
SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd");
String name = "Taro";
Date birthday = format.parse("19870929");
List<Date> vacations = new ArrayList<>();
vacations.add(format.parse("20161003"));
vacations.add(format.parse("20161005"));
vacations.add(format.parse("20161010"));
Person taro = new Person(name, birthday, vacations);
SimpleDateFormat printFormat = new SimpleDateFormat("yyyy/MM/dd");
for (Date vacation : taro.getVacations()) {
System.out.println(printFormat.format(vacation));
}
taro.getVacations().get(0).setTime(System.currentTimeMillis());
for (Date vacation : taro.getVacations()) {
System.out.println(printFormat.format(vacation));
}
}
}
2016/10/03
2016/10/05
2016/10/10
2016/09/29 //書き換えられた!
2016/10/05
2016/10/10
書き換えられてしまいました。
taro
インスタンスの vacations
インスタンス自体は不変ですが、その中の要素はImmutableTest.java の vacations
変数が持つ要素と同じインスタンスであるため、このままでは外から書き換えることが可能です。
それを防ぐためには、リスト内の要素についても防衛的コピーを適用する必要があります。
また、getterメソッドも同様にします。
public class Person {
private String name;
private Date birthday;
private List<Date> vacations;
public Person(String name, Date birthday, List<Date> vacations) {
this.name = name;
this.birthday = new Date(birthday.getTime());
// リストを新しく作り、中の要素についてもひとつずつ防衛的コピーをする。
this.vacations = new ArrayList<>();
for (Date vacation : vacations) {
this.vacations.add(new Date(vacation.getTime()));
}
}
public List<Date> getVacations() {
// インスタンスのvacationsインスタンスだけでなく、中の要素もひとつずつ防衛的コピーをしてから返却する。
List<Date> vacations = new ArrayList<>();
for (Date vacation : this.vacations) {
vacations.add(new Date(vacation.getTime()));
}
return vacations;
}
}
ImmutableTest.java
はそのまま変えずに実行すると、以下のようになります。
2016/10/03
2016/10/05
2016/10/10
2016/10/03 //書き換えられない!
2016/10/05
2016/10/10
これで、リストを渡す場合でも不変性を実現することができました。
まとめ
ここまで、オブジェクトを不変にするための工夫を紹介してきましたが、実際はもしかしたら完全に不変なオブジェクトを作るという機会はあまりないかもしれません。開発者がお互いにコミュニケーションをとれる状況なのであれば、不用意にインスタンスの内容を書き換えない、参照値を引き回さない、といった「使う側」のコーディングで対処できる場合があるからです。また、防衛的コピーは不変性を実現する一方で、getするたびに new
が呼ばれてしまう、というデメリットもあります。
Effective Javaにも繰り返し書かれているのですが、このようなテクニックが常に正しいとは限りません。要件や状況に応じて使い分ける必要があります。
とはいえ、Javaで他のクラスにインスタンス(の参照値)を渡すことによってどのような危険性が生じるのかを知る、という意味で、不変オブジェクトの考え方を知っておくのはとても良い勉強になるかと思います。