The King's Museum

ソフトウェアエンジニアのブログ。

【Effective Java】項目15:可変性を最小限にする

クラスの可変性は最小限にするべきである。

不変クラス

不変クラスとは、そのインスタンスを変更できないという性質をもつクラス。 インスタンスが生成された時点ですべての情報を持っていて、インスタンスの生存期間中はそれらの情報が変化しないことが保証されている。

Java ライブラリには String、ボクシングされた基本データクラス、BigInteger、BigDecimal などの多くの不変クラスがある。 不変クラスが好まれる理由は、設計、実装、利用が可変クラスよりも簡単になるからである。

クラスを不変にするためには以下の指針に従う。

  • オブジェクトの状態を変更するためのメソッドを提供しない
    • すなわち、セッターを提供しない
  • クラスが拡張できないことを保証する
  • すべてのフィールドを final にする
  • すべてのフィールドを private にする
  • 可変コンポーネントに対する独占的アクセスを保証する
    • クラスが可変オブジェクトを参照している場合、クライアントがそのオブジェクトへ参照できないことを保証する
    • コンストラクタ、アクセッサー、readObject メソッド内では、防御的コピー(項目39)を利用する

不変クラスのメリット・デメリット

例として、以下のような複素数を示す不変クラスを考えてみる。

public class Complex {
    private final double re;
    private final double im;

    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }

    public double realPart() { return re; }
    public double imaginaryPart() { return im; }

    public Complex add(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }

    public Complex subtract(Complex c) {
        return new Complex(re - c.re, im - c.im);
    }

    public Complex multiply(Complex c) {
        return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
    }

    public Complex divide(Complex c) {
        double tmp = c.re * c.re + c.im * c.im;
        return new Complex(
            (re * c.re - im * c.im) / tmp,
            (re * c.im + im * c.re) / tmp);
    }

    ...(hashCode などは省略)...
}

このクラスでは、各種の算術操作がインスタンス自身を変更するのではなく、新しいインスタンスを生成している。

このように、メソッドオペランドを変更しない方法は関数的な手法として知られている。 一方、オペランドを変更するような方法は手続き的、あるいは命令的と呼ばれている。

不変クラスのメリット・デメリット

関数的な方法で生成される不変オブジェクトは単純であり、コンストラクタで強制された不変式を破ることがない。 加えて、不変オブジェクトはスレッドセーフである。

そのため、以下のように、頻繁に利用される値を制限なく共有することができる。

public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);

さらに応用として static ファクトリーメソッドを介して、インスタンスをキャッシュすることができる。 すべてのボクシングされたデータ型と BigInteger は、そのような static ファクトリーメソッドを持っている。

この方法では、防御的コピー(項目30)をする必要もなく、clone やコピーコンストラクタ項目11)を作成する必要もない。 初期の Java の頃はこれが十分理解されず、String クラスはコピーコンストラクタを持っている。しかし String のコピーコンストラクタは使用されるべきではない(項目5

不変オブジェクトはそれ自体だけでなく、内部実装の状態さえも共有可能である。 BigInteger クラスは内部的な実装として「符号」と「大きさ」で値を表現しており、符号は int 型変数、大きさは int 配列で実装している。 たとえば、符号を反転する negate メソッドでは、大きさを示す int 配列はもともとのインスタンスとオブジェクトを共有していて、符号だけを変更した BigInteger インスタンスを新たに生成している。

不変クラスのデメリットは個々の異なる値に別々のオブジェクトを生成してしまうことである。

大きな値を持つ BigInteger インスタンスで、その最下位ビットだけを反転した場合にも、新たな大きな値を持つ BigInteger インスタンスを生成してしまう。 一方、BitSet ではインスタンス自体を変更するので 1 ビットの状態を変更するだけですむ。

複数ステップの操作がある場合、ステップごとにオブジェクトを生成することになるためパフォーマンスが劣化する。 解決方法としては、複数ステップを一つの操作とする方法がある。 他の方法としてパッケージプライベートの可変のコンパニオンクラスを作成し、内部的にはそのコンパニオンクラスを利用するという方法もある。

static ファクトリーメソッド

不変性を保証するため、不変クラスではサブクラス化を許さないことが必要である。 クラス自体に final キーワードを与えてもよいが、コンストラクタを private、またはパッケージプライベートにする方法がある。

Java ライブラリの BigInteger と BigDecimal が書かれた当時は、クラスを final にする重要性が理解されておらず、これらのクラスの持つメソッドはすべてオーバーライド可能になっている。 そのため、セキュリティへの配慮が必要なメソッドで BigInteger や BigDecimal を期待する場合には、getClass() を用いて本当に BigInteger や BigDecimal かどうかを確認する必要がある。 確認しない場合、BigInteger のサブクラスが渡され、内部的な状態が露呈する可能性があるからである。

キャッシュへの利用

外部に分かるオブジェクトが変更されなければ、不変クラスは内部的な状態を変更することができる。 これを利用して、コストの高い計算を最初に行い、内部的にその値をキャッシュしておくことが可能。

例えば、PhoneNumber の hashCode は、PhoneNumber が不変クラスなのでハッシュ値をキャッシュすることが可能である。 この方法は遅延初期化(項目71)の古典的な方法であり、String クラスでも利用されている。

なお、PhoneNumber や Complex などの小さなクラスは常に不変クラスにするべきだ。 Java ライブラリには java.util.Date や java.awt.Point など、不変にするべきだが、そうなっていないクラスが多くある。

また、クラスを不変にすることが現実的ではないクラスでは、その可変性を最小限にするべき。 オブジェクトの存在する状態の数を減らすことでエラーが劇的に減少する。

最後に、オブジェクトは不変式が確立した状態で生成されるべきである。 やむを得ない理由がない限り、明示的な初期化メソッドは提供しないほうがよい。 TimerTask クラスはこの原則のよい例となっている。 TimerTask クラスの状態遷移空間は意図的に小さく設計されており、TimerTask が生成され、キャンセル・終了すると、それは再スケジュールすることができなくなっている。

感想

なんか長くなった。

可変性の話は SICP を思い出した。 可変性を導入すると参照等価性が失われるとかなんとか、、、

考慮する状態の数が減るのはけっこう重要な利点だと思っていて、final になってるフィールドが多いと安心して変更ができる。

(c) The King's Museum