「わかりやすいJava入門編」、「わかりやすいJava オブジェクト指向徹底解説」で解説しているイミュータブルなクラス(不変クラス)の作成方法について、さらに詳しく書き直しました。これまで、基本的な方法だけを解説していたのですが、質問があったので、応用的な方法の解説を追加しています。
#1.イミュータブル(immutable)なクラスとは
普通のクラスは、フィールド変数の値をいつでも変更できるので、ミュータブル(mutable:変更できる)なクラスですが、フィールド変数の値を変更できないクラスはイミュータブル(immutable:変更できない)なクラスといいます。それはフィールド変数の値を一度決めたら、後からは変更できないクラスです。イミュータブルなクラスなら、他のメソッドに参照を渡しても、内容を変更されることはありません。
##2.イミュータブルなクラスを作る基本的な方法
イミュータブルなクラスを作る基本的な方法は次のようです。
①のセッターメソッドを作らない、だけで十分なように思えるかもしれませんが、それだけではイミュータブルにできません。なぜなら、セッターでなくてもフィールド変数の値を変更するようなメソッドを作ることができるからです。そこで、②のようにフィールド変数にfinal修飾子を付けます。
final修飾子は「変更できない」という意味で、フィールド変数に付けると、「一度、値を設定したら、後からは変更できない」という意味になります。これにより、フィールド変数の値を変更するようなメソッドを作れなくなります。
しかし、それでもまだ十分ではありません。5章で解説する継承という機能を使うと、クラスデザインを変更してイミュータブルでないクラスにできるので、この機能を使えなくしておく必要があります。そこで、クラス宣言にもfinalを付けておきます。これにより、クラスは継承が使えなくなるのです。
イミュータブルなProductクラスの例を示します。
1 package sample2;
2 import java.time.LocalDate;
3 public final class Product {
4 private final String number; // 型番
5 private final String name; // 品名
6 private final int price; // 価格
7 private final LocalDate date; // 発売日
8 private final boolean stock; // 在庫(の有無)
9 // コンストラクタでは必ずフィールド変数に値を設定しなければならない
10 public Product(String number, String name,
11 int price, LocalDate date, boolean stock) {
12 this.number = number;
13 this.name = name;
14 this.price = price;
15 this.date = date;
16 this.stock = stock;
17 }
18 // セッターは作成しない
19 // ゲッター、toStringメソッドの掲載を省略
19 }
セッターを作らず、クラスの修飾子は public final、フィールド変数も private final とします。
なお、フィールド変数にfinal が付いているので、フィールド変数には必ず初期値を代入しておかねばなりません。そのため、初期値を設定するコンストラクタが必要です。適切なコンストラクタがないとコンパイルエラーになるので注意してください。
#3.可変オブジェクトを含むクラス
StringやLocalDateクラスは、イミュータブルになるように作成してあるので、セッターがないのはもちろん、一度作成したインスタンスの値を後から書き換えるようなメソッドは一切ありません。ですから、例題は完全にイミュータブルなクラスです。
では、そうではない例を見てみましょう。
次のMemberクラスは、IDと名前を持つクラスで、基本的な方法①~③によりイミュータブルな形式にしています。つまり、クラスとフィールド変数をfinalにし、メソッドはゲッターとtoString()だけで、セッターは作っていません。
public final class Member {
private final IdNumber id; // イミュータブルでないクラスのインスタンス
private final String name;
// コンストラクタ、ゲッター、toStringメソッドの記載を省略
・・・
}
ただ、フィールド変数idは、IdNumber型のオブジェクトです。IdNumberクラスはint型のnumberをフィールド変数に持つ普通のクラスで、次のように、イミュータブルなクラスではありません。
public class IdNumber {
private int number; // 番号
// コンストラクタ、ゲッター、セッター、toStringメソッドの記載を省略
・・・
}
このような可変(ミュータブル)なオブジェクトを含むMemberクラスは、基本的な方法だけではイミュータブルにならないことを確かめてみましょう。
まず、Memberクラスのインスタンスを作って表示してみます。
public class Exec {
public static void main(String[] args) {
IdNumber id = new IdNumber(100); // IdNumbeのインスタンスを作成
Member member = new Member(id, "田中宏"); // Memberのインスタンを作成
System.out.println(member); // memberの内容を表示する
}
MemberクラスにはtoStringメソッドが作成してあるので、次のように出力されます。
Member [id= IdNumber [number=100] , name=田中宏]
idの値ですが、"IdNumber [number=100]"の部分は、メンバであるIdNumberクラスのtoStringメソッドにより表示されています。
では、idの値は変更できるでしょうか?セッターは作成していないことに注意してください。少し考えるとわかると思いますが、まずidをゲッターで取得し、次に、IdNumberクラスのセッターを使います。つまり、id.setNumber(~)で値を変更できます。そこで、Execクラスに次の3行を書き足して実行してみます。
IdNumber _id = member.getId(); // idを取り出す
_id.setNumber(200); // IdNumberクラスのセッターで200に変更する
System.out.println(member); // memberの内容を表示する
idの値を変更できてしまうので、memberを出力してみると、そのidが200に変更されていることがわかります。
Member [id=IdNumber [number=100], name=田中宏]
Member [id=IdNumber [number=200], name=田中宏]
以上で、Memberクラスがイミュータブルでないことがわかりました。
#4.クローンコピーを使ってイミュータブルにする
Memberクラスのフィールド変数idのように、イミュータブルなクラスが、可変(ミュータブル)なオブジェクトをフィールド変数に持つ場合は、次のようにします。
基本的な方法に加えて、さらに、①、②を適用すると、Memberクラスは次のようになります。
1 package sample4;
2 public final class Member {
3 private final IdNumber id;
4 private final String name;
5 public Member(IdNumber id, String name) {
6 this.id = new IdNumber(id.getNumber()); // コピーを作成して代入する
7 this.name = name;
8 }
9 public IdNumber getId() {
10 return new IdNumber(id.getNumber()); // コピーを作成して返す
11 }
12 public String getName() {
13 return name;
14 }
15 @Override
16 public String toString() {
17 return "Member [id=" + id + ", name=" + name + "]";
18 }
19 }
5行目のコンストラクタでは、受け取ったidのコピーを作成して、それをフィールド変数に代入しています。また、9行目のゲッターでは、フィールド変数のコピーを作成してそれを戻り値として返しています。
コンストラクタとゲッターで元のオブジェクトのクローンコピーを使うことにより、クラスの外部で、参照を通じてidの値が変更されても、Memberクラスには一切影響を及ぼさなくなります。
試してみましょう。次のExecクラスでは、idの値を取り出して、100から200へ変更します。
1 package sample4;
2 public class Exec {
3 public static void main(String[] args) {
4 IdNumber id = new IdNumber(100);
5 Member member = new Member(id, "田中宏");
6 System.out.println(member);
7 IdNumber _id = member.getId(); // idを取り出す
8 _id.setNumber(200); // IdNumberクラスのセッターで200に変更する
9 System.out.println(member); // memberの内容を表示する
10 }
11 }
7行目でmemberからidの値を取り出して、_idに代入しますが、これはコピーされたクローンオブジェクトですから、8行目でその値を変更しても、9行目の出力ではmemberの値が変わっていないことがわかります。
Member [id=IdNumber [number=100], name=田中宏]
Member [id=IdNumber [number=100], name=田中宏]
コンストラクタで、コピーしたidをフィールド変数に代入するのも同じ理由です。memberインスタンスを作成するのに使ったidが他の場所で変更されても、memberインスタンスのidの値は変わりません。クローンコピーしたidをフィールド変数に代入しているからです。