NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

駆け出しエンジニアの成長日記 ~ミュータブルとイミュータブル編~

こんにちは。友野です。
2度目のブログ執筆です。前回の初執筆から2か月が経つと知って驚いています。本当に月日が経つのははやい。はやすぎる。

今回は、プログラム開発において重要な概念でもあるミュータブルとイミュータブルについてまとめてみました。

ミュータブルとイミュータブルについて知る

ミュータブル、イミュータブルとは

「イミュータブルなクラスを定義する」といった使い方で目にすることが多い用語ですが、
日本語に直訳すると以下のように表されます。

  • mutable:変えられる、変更可能な
  • immutable:変えることのできない、変更不可能な

一方で、プログラミングの世界では以下のような扱い方となります。

  • mutableなオブジェクト:フィールドの値が変更可能なオブジェクト
  • immutableなオブジェクト:フィールドの値が変更不可能なオブジェクト

ミュータブルとイミュータブルそれぞれのメリデメについては様々な意見があります。
ただ、結局どちらを使うべきなの?ミュータブルは悪なの?という感想を抱くのがオチです。

近頃の風潮として、処理結果に影響を与え得る機能については、意図しないオブジェクト状態の変更によるバグの発生を防ぐためにイミュータブルなプログラミングが重視されているようです。
ただ、推奨されているからと訳もなくイミュータブルを採用するよりかは、場面に合わせた使い分けができることが大切だと考えています。

ミュータブル、イミュータブルの使い分け

前提として、ミュータブルとイミュータブルの大きな差は「値が変えられるか変えられないか」、それだけです。

値が次々に変化する可能性があるオブジェクトを使用する場面では、ミュータブルを採用するほうがよいと考えられます。
変更したいプロパティがイミュータブルなオブジェクトの場合、新たにインスタンスを生成した上で変更したいプロパティに値を代入し、変更不要のプロパティには古いインスタンスから値をコピーしてくる、というやや冗長な処理が発生してしまうからです。

対して、イミュータブルな設計が望ましい例としては、データ読み取り専用アプリやマスタデータのキャッシュを行うような処理内容が該当します。
イミュータブルなオブジェクトは、生成したインスタンスの状態が他者に変えられることはないという安心感があります。
マルチスレッドでオブジェクトを共有する場合、イミュータブルは値が変わらないことが保証されているため安心して参照することができます。
ミュータブルだとどこでどのように変更が行われるか想定できないため、値が変えられる可能性を考慮した処理も考えなければなりません。

ミュータブルなオブジェクトかどうかによる実行時のパフォーマンスの違いに関しては、各状態のオブジェクトを何万回も参照・繰り返し実行してやっと差が生まれるような程度であり、人間では体感できない微量の差でしかないため性能面ではほとんど違いはないと考えられます。


ミュータブル・イミュータブルという言葉が用いられる一例として、データ型であるプリミティブ型・オブジェクト型の説明があります。変数の型はこれらのいずれかに分類されます。
Javaの仕様として、それぞれの型を引数に使用した時の挙動に、以下の様な違いがあります。

  • プリミティブ型:値渡し、呼び出し先で変更を加えても呼び出し元で値は変わらない
  • オブジェクト型:参照渡し、呼び出し先で変更を加えると呼び出し元で値が変わってしまう
    ※参照の値渡しと呼ばれることもあります。ここではJavaの引数の言語仕様は説明しきれないため、興味のある方は調べてみてください。

この説明に基づくと、プリミティブ型はイミュータブルの特性を持っています。よって、プリミティブ型に属するint型の変数はイミュータブルであると言えます。

以下のJavaプログラムでは、プリミティブ型のintとオブジェクト型のMutableDtoが、それぞれイミュータブル・ミュータブルな性質であることを示しています。

@Getter
@Setter
@ToString
public class MutableDto {
    private Integer id;
    private String name;
}

public class MutableSample {
    public static void main(String[] args) {
        MutableDto mutableDto = new MutableDto();
        mutableDto.setName("Apple");
        int b = 0;

        System.out.println(mutableDto); //MutableDto(id=null, name=Apple)
        System.out.println("b=" + b); //b=0

        sample(mutableDto, b);

        System.out.println(mutableDto); //MutableDto(id=null, name=Orange)
        System.out.println("b=" + b); //b=0
    }

    public static void sample(MutableDto mutableDto, int a) {
        mutableDto.setName("Orange");
        a = 9;
    }
}

イミュータブルなクラスに値をセットする

一般的に、値をセットする方法としてはSetterとBuilderが挙げられます。
パラメータに値をセットする場合のSetterとBuilderの違いと、イミュータブルなクラスへ値を設定する場合のことを考えてみます。


SetterとBuilderはどちらもメソッドですが、Builderはデザインパターンの一種であり、Builderパターンと呼ばれるものになります。

デザインパターンとは、ソフトウェア開発においてよく利用される設計方針をいくつかのパターンに整理し分類したもののことを指します。
デザインパターンを設計に取り込むことで開発者が得られるメリットは複数挙げられます。(興味のある方は調べてみてください。皆様ヒジョーに分かりやすくまとめてくださっています。)

中でも広く認知されているのが「GoFのデザインパターン」というものです。
オブジェクトの「生成」「構造」「振る舞い」の大分類を軸に全部で23種類のパターンに分けられており、そのうちのひとつにBuilderパターンが存在しています。

ただ、Builderパターンをjavaで実装する場合は、各クラスのコンストラクタやBuilderクラスの生成、関連する複数のメソッドが必要となり、必然的にコード量が増えてしまいます。
後述のBuilderを実装したサンプルコードでは、Lombokの@Builderアノテーションを使用しています。
クラスに@Builderアノテーションを付与するだけでBuilderのコードを自動生成してくれるため、簡潔な記述でBuilderパターンの実装が実現できます。

SetterとBuilderの違い

両者ともオブジェクトに値を設定するという意味では同じ処理内容を提供していますが、イミュータブルな実装にする場合はBuilderの使用をお勧めします。
理由としては、SetterとBuilderそれぞれでインスタンスを生成した場合のオブジェクト状態の違いが関係しています。

そもそも、SetterとBuilderではインスタンス化されるタイミングが異なります。
そのタイミングの異なりから、Setterはインスタンス生成前後でオブジェクトの状態が変わってしまいますが、一方のBuilderは不変となります。
SetterとBuilderそれぞれでパラメータに値を設定する場合の過程で確認してみましょう。

Setter
Setterはインスタンスを生成した後に値を設定するため、オブジェクトの状態が変わってしまいます。

@Getter
@Setter
public class SetterSample {
    private Integer id;
    private String name;
}

public class BuilderSample {
    public static void main(String[] args) {
        SetterSample sample = new SetterSample(); //ここでインスタンス化
        sample.setId(10); //インスタンス生成後でも自由に値を変更することが可能
        sample.setName("Bob");
    }

Builder
Builderは値を設定してからインスタンスを生成するため、オブジェクトの状態は不変となります。
build()でインスタンスを生成しています。

@Getter
@Builder
public class BuilderSample {
    private Integer id;
    private String name;
}

public class BuilderSample {
    public static void main(String[] args) {
        BuilderSample sample = BuilderSample.builder()
                     //インスタンス生成前に値を設定
                     .id(10)
                     .name("Bob")
                     .build(); //ここでインスタンス化
    }

よって、イミュータブルなクラスに値をセットするには、インスタンス生成前後でオブジェクト状態が変化しないBuilderを使用するべきだといえます。

また、Setterを使用せずともコンストラクタを用いることで値の設定は可能ですが、引数の順番や必須の要素について過不足なく記載する必要があり、値の渡し間違えが起こるリスクも考えられます。
その点、Builderは値を渡す順番にも決まりがなく、値の設定が不要なプロパティにわざわざnullを指定する必要もありません。
Builderは柔軟に値を設定することができる便利なメソッドです。

おわりに

処理速度などの性能向上というよりは、プログラマーにとって負担の少ない設計を手助けしてくれるミュータブル、イミュータブルの概念。オブジェクト指向において基本事項でありながらもあまり意識してこなかった部分だったので、今後は設計基準に重要な要素として扱っていきたいと思います。
最後までお読みいただき、ありがとうございました。