不変オブジェクトは安全~とか、できるだけ不変オブジェクトを作るようにしろ~とか、聞いたことはあるけどそれってなんなの?って人多いと思います
ここでは不変オブジェクトとは何なのか、そして不変オブジェクトはどう作るのかについて説明していこうと思います
不変オブジェクトは言語関係なく作れるのですが、Javaで説明していこうかなと思います
ここでは初心者向けに解説していますが、より詳細な説明が読みたい方は是非下記の書籍を買ってみてください
Effective Java 第3版
#不変オブジェクトとは
ちょーかんたんに言うと、持っているデータが変わらないオブジェクトのことです
意味が分からないと思うので、JavaのStringとStringBuilderを使って説明していきます
まずは下記のコードを見てください
String str = "A";
for(int i = 0; i < 2 ; i++){
str += "A";
}
System.out.println(str);
出力結果は下記のようになります
AAA
このコードは単純にstrにAを2回足しています
上記のコードではstrをString型で宣言していますが、StringBuilder型でstrを宣言しても出力結果は変わりません
しかし、StringとStringBuilderではインスタンス生成のやり方が違います
最終的にどのようなインスタンスが残るのか、見てみましょう
StringBuilderの場合
単純に出力結果と同じインスタンスしか生成されていないため、最終的に残るインスタンスもAAAのひとつだけです
Stringの場合
Stringの場合はAが足される度に別のインスタンスを生成しています
そのため、A、AA、AAAのインスタンスが残ります
A、AAのインスタンスは参照が外れているため、GCの対象となっています
###StringとStringBuilderの違い
上記の例を見てもらえるとわかると思いますが、StringBuilderはひとつのインスタンスの値が書き換わっているのに対して、Stringは値を書き換えずに別のインスタンスを生成しています
StringBuilderはインスタンスを1回しか生成しないため、負荷が低くて済みます
Stringはインスタンスを何回も生成しているため、その分負荷が高くなります
これだけ見るとStringなんて使う意味ない!と感じるかもしれませんが、それは間違いです
###不変オブジェクトと可変オブジェクト
StringBuilderは値を書き換えているため可変オブジェクトと呼ばれます
Stringは値を書き換えていないため不変オブジェクトと呼ばれます
StringBuilderの長所は前述している通り、パフォーマンスの改善に繋がります
今回の例ではAを2回しか結合していないですが、これが10000回とかになるとインスタンスの生成コストを馬鹿にできなくなってきます
しかし、同じインスタンスなのにタイミングによってAだったりAAだったりAAAAAAAだったり、値が書き換わってしまいます
これが大問題で、変更を一回加えるとそのオブジェクトを参照しているオブジェクトにも影響が出てしまいます
つまり、可変オブジェクトを使うと変更が加わる度に何かしらの副作用が発生します
Stringの場合はどうなるでしょうか
インスタンスを複数生成するためコストは掛かりますが、ひとつのオブジェクトは常に同じ値を保持し続けます
別の値を持ったインスタンスが欲しくなった場合は、別のインスタンスを生成します
そのため、他のオブジェクトに影響を出さずに済みます
つまり、不変オブジェクトを使うとその値が変更されないことを保証することができます
不変オブジェクトはマルチスレッドプログラミングにも役立ちます
不変オブジェクトは値が変わらないため、synchronizedで同期したり…ということをやらなくて済みます
マルチスレッド環境で問題になるのは可変オブジェクトだからです
不変オブジェクトは、本質的にスレッドセーフになります
もし可変オブジェクトを作るにしても、できる限り可変な部分を減らすことによって事故を未然に防ぐことに役立ちます
#不変オブジェクトの作り方
ここまでの説明で不変オブジェクトがどれだけ有用か分かって頂けたと思います
ここからは、実際にどのように不変オブジェクトを作るのかを説明していこうと思います
###クラスを継承できないようにする
クラスを継承できるようにしてしまうと、継承されて変なメソッドを追加されたりするなど、カプセル化が破られてしまいます
型が同じなのに生成のされ方によって不変オブジェクトだったり可変オブジェクトだったりする…というような問題を起こしたくないため、継承できないようにclassを宣言します
final class TestClass{}
カプセル化されているモジュールのクラスであれば外部から見えないのでfinalを付けなくても実質的に継承は不可能ですが、継承されることを意図していない場合にはfinalを付けて継承を禁止することが一般的です(これは可変オブジェクトにも同じことが言えます)
###すべてのフィールドを定数にする
不変オブジェクトは値を変更されたく無いので、すべて定数にします
また、直接変数を参照されないように可視性を設定します
private final int num;
private final StringBuilder str;
基本データ型、参照データ型に関わらず同じように宣言してしまって問題ないです
参照データ型の場合は少し扱いを変える必要があるのですが、後述します
###ミューテーターを作らない
ミューテーター(mutator)とは、セッターをはじめとする値を書き換える操作のことです
値は書き換えたくないので、勿論そのような操作をするメソッドは必要ありません
###ゲッターに気を付ける
まずは下記コードを見てください
public int getNum(){
return num;
}
public StringBuilder getStr(){
return str;
}
getNumメソッドは戻り値が基本データ型になっており、getStrメソッドは戻り値が参照データ型になっています
基本データ型の場合、numの値のコピーが返されますので特に問題はありません
しかし参照データ型の場合、参照をそのまま返してしまうため問題が発生します
getStrメソッドを呼び出した側でインスタンスの参照を取得できてまうため、strインスタンスの中身の値を書き換えることが出来てしまいます
それを防ぐため、先ほどのコードを改良してみました
public String getStr(){
String result = str.toString;
return result;
}
StringBuilderクラスにはtoStringメソッドという可変オブジェクトを不変オブジェクトに変えるメソッドがあったので使ってみました
しかし、「ゲッターに気を付ける」の説明でこの型変換の部分はあまり重要ではありません
重要なのは、値をコピーして返しているということです
別のコピーインスタンスを生成しておき、コピーインスタンスの参照を返します
これをすることによってgetStrメソッドを呼び出した側でいかなる手段を使っても、コピーインスタンスをいじることはできますが、元のインスタンスに干渉することはできなくなります
この技法を防御的コピーと呼びます
防御的コピーを使うことによって、StringBuilderのような可変オブジェクトを扱っているクラスでも、クラスを不変オブジェクトとすることができます
防御的コピーを使うときの注意点があります
①パフォーマンスが劣化する可能性があります
配列やListなどの場合、中身を全てコピーしなければならないためパフォーマンスが劣化する可能性があります
しかし、不変オブジェクトにはその代償を払うだけの価値があることを忘れないでください
②cloneメソッドを使わないでください
javaのcloneメソッドはいくつかの問題があるため、防御的コピーをする時利用しないでください
#####サンプルコード
ここまでで説明した不変オブジェクトのサンプルコードを置いておきます
(コンストラクター部分が追記されています)
final class TestClass{
private final int num;
private final StringBuilder str;
public TestClass(int num,StringBuilder str){
this.num = num;
//ここも防御的コピーを行う
this.str = new StringBuilder(str);
}
public int getNum(){
return num;
}
public String getStr(){
String result = str.toString;
return result;
}
}
※2020/7/1追記 (@saka1029 さん、指摘ありがとうございます)
コンストラクターで可変オブジェクトを引数として受け取る時にも防御的コピーを行う必要があります
これを行わなかった場合、呼び出し元とTestClassで同じ参照先を持つことになってしまい、値を書き換えることができてしまいます
可変オブジェクトを受け取る時も、渡す時も、防御的コピーを使ってください
###おまけ
上記のサンプルコードではコンストラクターをpublicにしていますが、Effective Javaではstatic factoryが推奨されています
これは現在のAPI開発にも言えます
先ほどのサンプルコードを少し書き換えてみます
final class TestClass{
private final int num;
private final StringBuilder str;
//外部からコンストラクターの使用を禁止している
//呼び出し側で下記のコードが使えなくなる
// TestClass tc = new TestClass(10,"AAA");
private TestClass(int num,StringBuilder str){
this.num = num;
this.str = new StringBuilder(str);
}
//static factory
public static final TestClass newInstance(int num,StringBuilder str){
return new TestClass(num,str);
}
public int getNum(){
return num;
}
public String getStr(){
String result = str.toString;
return result;
}
}
呼び出し側のコードは下記のようになります
TestClass tc = TestClass.newInstance(10,"AAA");
上記のコードの場合、static factoryであるnewInstanceメソッドが呼ばれる度に新しいTestClass型のインスタンスが生成されるため、シングルトンにはなりません
が、シングルトンにしたくない場合にはこの技法は使えます
シングルトンにしたい場合にはstatic factoryメソッド内でnewを使わず、予めインスタンスを作っておきreturnするだけにします
DIコンテナの登場でstatic factoryを直接呼び出す機会は減っているかもしれませんが、この技法は未だに現役です
#まとめ
不変オブジェクトとは何なのか、どうやって作るのか、というのを紹介してみました
どんなシステムでも不変オブジェクトが必要無いシステムは存在しないと言っても過言ではないため、是非覚えて使ってみてください