Effective Java で説かれているのが、不変クラスの重要性だ。以前、読書メモを以下の記事に書いた。
不変クラスとは、インスタンス生成後は保持データの変更ができないクラスのことだ。例えば、Java の String や基本データクラスは不変クラスだ。ある String のインスタンスが持つ文字列を変更することはできない。
不変クラスは主に以下の点で便利だ。
- 単純で扱いやすい
- マップやセットなど、他のオブジェクトで利用しやすい
- スレッドセーフである
あるクラスを不変クラスとするためには、いくつかの条件がある。それを説明するために、ISBN を例として Java で不変クラスを作ってみる。本記事では、
- ISBN
- 不変クラスの条件
について説明する。
ISBN とは
ISBN は書籍を識別するための番号だ。本の裏のコレである。
詳しくは Wikipedia を参照してほしい。
今回は現行規格である 13 桁 ISBN を保持するクラスを作る。ISBN クラスには以下のメソッドを実装する。
- String で渡した ISBN からインスタンスを生成するメソッド
- 接頭、グループ、出版社、書名、チェックディジットの各部分を取得するメソッド
- ある String が ISBN として正当かどうか確かめるメソッド
- ある 10 桁 ISBN を 13 桁 ISBN に変換するメソッド
ISBN 自体はただの数字の羅列であり、状態を持たない。よって、クラスとして実現するのであれば、不変クラスとすべきだろう。
不変クラスの条件
ISBN を不変クラスとして作っていくために、不変クラスの条件について説明する。条件は以下の通り。
- setter のような、オブジェクトの状態を変更できるメソッドを持たない
- サブクラスで状態を変更されないように、クラスの拡張を防ぐ
- すべてのフィールドの可視性を private にする
final
をつけるなどしてすべてのフィールドを変更不可にする- 可変オブジェクトを持つ場合、その不変クラスだけが変更できるようにする
各項目とも、外部からの変更を防ぐことを目的としたものである。項目 2 と 5 について、注意点を説明する。
2. クラス拡張禁止について
クラスの拡張を防ぐ方法として、Java であれば、以下のように final
を利用できる。
public final class Isbn { //...
これ以外にも、コンストラクタを private にする方法がある。コンストラクタが private だと、そのクラスを継承したとき、その子クラスから親クラスのコンストラクタを呼び出せないため、拡張できない。
しかし、そのままだと、その不変クラスを生成できない。そこで、以下のような static ファクトリメソッドを定義する。
public class Isbn { // static ファクトリメソッド(単純な例) public static Isbn of(String arg) { return new Isbn(arg); } // private なコンストラクタ private Isbn(String arg) { //... } }
これで、Isbn.of("978-4-***-*****-*")
のようにすることでインスタンスを生成できる。static ファクトリメソッドのほうが柔軟性は高いので、今回はこの方法をとる。static ファクトリメソッドの詳細は以下の記事を参照してほしい。
5. 可変オブジェクトについて
可変オブジェクトを外部から変更されないようにするためには、防御的なコピーが必要となる。
ISBN とは異なるが、例えば Person
クラスを名前、誕生日をフィールドに持つ不変クラスとしたいときを考える。誕生日には可変クラス Date
を利用する。このとき、コンストラクタや getter を以下のように実装してはいけない。
public final class Person { private final String name; private final Date birthday; // ダメなコンストラクタ public Person(String name, Date birthday) { this.name = name; this.birthday = birthday; } // ダメな getter public Date birthday() { return birthday; } }
なぜなら、このクラスを利用するクライアントから、以下のように date
を(Date.setMonth
は非推奨メソッドながら)操作できてしまい、Person
が不変クラスとして成り立たなくなってしまうからである。
DateFormat format = new SimpleDateFormat("yyyy/MM/dd"); Date date = format.parse("1940/10/09"); Person john = new Person("John Lennon", date); // Wed Oct 09 00:00:00 JST 1940 System.out.println(john.birthday()); // 外部から john の内部を操作 date.setMonth(0); // Tue Jan 09 00:00:00 JST 1940 System.out.println(john.birthday());
これを防ぐために、以下のように防御的にコピーするのが重要である。
public final class Person { public Person(String name, Date birthday) { this.name = name; // 防御的コピー this.birthday = new Date(birthday.getTime()); } public Date birthday() { // 防御的コピー return new Date(birthday.getTime()); } }
これで、Person
の内部と外部で Date
インスタンスが別々のものとなり、外部から birthday
を操作できなくなるため、不変クラスといえることになる。
とはいえ、今回の ISBN では可変オブジェクトを持たせない予定なので、この条件については気にしないことにする。
次回
不変クラスの条件を守った ISBN のクラスを作ってみる。
参考文献
EFFECTIVE JAVA 第2版 (The Java Series)
- 作者: Joshua Bloch,柴田芳樹
- 出版社/メーカー: 丸善出版
- 発売日: 2014/03/11
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (12件) を見る