Javaでimmutableを試す

Javaでimmutableオブジェクトを試してみたメモです。

Javaでimmutableオブジェクトを作成してみました。

immutable

immutableは不変オブジェクトです。
作成後は変更できません。
immutableなオブジェクトを使用することで状態の変化によるリスクを気にする必要がなくなります。

JavaだとStringはimmutableなオブジェクトになっています。

Oracleのドキュメントを見てみると、immutableの簡単な指針は下記のようです。
A Strategy for Defining Immutable Objects (The Java™ Tutorials > Essential Classes > Concurrency)

  • setterメソッドを提供しない。
  • すべてのフィールドをfinalかつprivateにする。
  • サブクラスによるメソッドのオーバーライドを許可しない。(クラスをfinalとして宣言、またはコンストラクタをプライベートにして、ファクトリメソッドでインスタンス生成)
  • インスタンスフィールドにmutableなオブジェクトへの参照が含まれている場合
    • mutableなオブジェクトを変更するメソッドを提供しない。
    • mutableなオブジェクトへの参照を共有しない。
    • コンストラクタに渡された外部のmutableなオブジェクトへの参照を保存しない。必要に応じてコピーを作成し、コピーへの参照を保存する。同様に、メソッド内でオリジナルを返さないように、必要に応じてmutableオブジェクトのコピーを作成する。

immutableなクラス

immutableなPersonクラスを作成してみました。
immutableとなっているポイントとしては下記の通りです。

  • クラスをfinalとして宣言
  • すべてのフィールドがfinal private
  • setterメソッドを提供してない
  • フィールドにmutableなオブジェクトの参照を含んでいない

Person.java

public final class Person {
    private final String name;
    private final String country;
    private final int age;

    public Person(String name, String country, int age) {
        this.name = name;
        this.country = country;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public String getCountry() {
        return country;
    }

    public int getAge() {
        return age;
    }
}

Personインスタンスを作成してみます。
immutableなので、変更しようとするとエラーとなります。

@Test
public void personTest() {
    final Person person = new Person("Anne", "Japan", 28);
    // error
    // person.setName("Bobby");
    // person.name("Cindy");
}

mutableなクラスのフィールドを持つimmutableなクラス

下記のようなmutableなクラスを定義。

MyMutable.java

public final class MyMutable {
    public String message;
    public int code;

    public MyMutable(String message, int code) {
        this.message = message;
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }
}

下記の様なフィールドにmutableなクラスを持ったPersonHasMutableクラスを定義しました。
Personクラスと同様のフィールドに加えて、MyMutableを持つフィールドがあります。
コンストラクタで渡されたMyMutableの参照を保持すると、外部でそのオブジェクトが変更される可能性があります。
そのため、コンストラクタ内で新しいMyMutableを生成し、外部のmutableなオブジェクトの参照を保持しないようにしています。

PersonHasMutable.java

public class PersonHasMutable {
    private final String name;
    private final String country;
    private final int age;
    private final MyMutable myMutable;

    public PersonHasMutable(String name, String country, int age, MyMutable myMutable) {
        this.name = name;
        this.country = country;
        this.age = age;
        this.myMutable = new MyMutable(myMutable.getMessage(), myMutable.getCode());
    }

    public String getName() {
        return name;
    }

    public String getCountry() {
        return country;
    }

    public int getAge() {
        return age;
    }

    public MyMutable getMyMutable() {
        return myMutable;
    }
}

PersonHasMutableを生成してみます。
コンストラクタで渡しているMyMutableオブジェクトを変更しても、
新しいMyMutableを作成して保持しているため、値は変更されません。

@Test
public void personHasMutableTest() {
    final MyMutable mutable = new MyMutable("aaa", 111);
    final PersonHasMutable personHasMutable = new PersonHasMutable("Anne", "Japan", 28, mutable);

    // mutable更新
    mutable.setMessage("bbb");
    mutable.setCode(200);

    assertThat(personHasMutable.getMyMutable().getMessage()).isEqualTo("aaa");
    assertThat(personHasMutable.getMyMutable().getCode()).isEqualTo(111);
}

Lombok

Lombokの自動生成がimmutableなクラスを作るのに便利です。

Lombok.Value

Lombokの@Valueはフィールドをprivate finalにします。
さらにクラスをfinalにします。
フィールドのゲッターのみが生成され、セッターは生成されません。

@Valueを利用すると、はじめに作ったPersonクラスは下記のように書けます。

PersonWithValue.java

@Test
public void lombokValueTest() {
    PersonWithValue person = new PersonWithValue("anna", "Japan", 28);

    // error
    // person.setName("Bobby");
    // person.name("Cindy");
}

インスタンスを作成して変更しようとするとエラーとなります。

@Test
public void lombokValueTest() {
    PersonWithValue person = new PersonWithValue("anna", "Japan", 28);

    // error
    // person.setMessage("Bobby");
    // person.message("Cindy");
}
Lombok.Builder

Lombokの@Builderはビルダーパターンを提供します。
build()で生成されたインスタンスは変更できません。

@Builderを利用すると、はじめに作ったPersonクラスは下記のように書けます。

PersonWithBuilder.java

@Builder
public final class PersonWithBuilder {
    private String name;
    private String country;
    private int age;
}

インスタンスを作成して変更しようとするとエラーとなります。

@Test
public void lombokValueBuilderTest() {
    PersonWithBuilder person = PersonWithBuilder
            .builder()
            .name("anna")
            .country("Japan")
            .age(28)
            .build();

    // error
    // person.setName("Bobby");
    // person.name("Cindy");
}

immutableなCollection

Collections.unmodifiableXXXX

java標準のCollections.umodifiableXXXで変更不可の読み取り専用の各コレクションを取得できます。
下記のコレクションに対してメソッドが用意されています。

  • unmodifiableCollection(Collection c)
  • unmodifiableList(List list)
  • unmodifiableMap(Map m)
  • unmodifiableNavigableMap(NavigableMap m)
  • unmodifiableNavigableSet(NavigableSet s)
  • unmodifiableSet(Set s)
  • unmodifiableSortedMap(SortedMap m)
  • unmodifiableSortedSet(SortedSet s)

unmodifiableList()でListを生成してみます。
add()で要素を追加すると、UnsupportedOperationExceptionが発生します。

@Test(expected = UnsupportedOperationException.class)
public void unmodifiableListTest() throws UnsupportedOperationException {
    final List<String> list = new ArrayList<>();
    list.add("aaa");
    list.add("bbb");
    final List<String> unmodified = Collections.unmodifiableList(list);

    // addの操作自体はコンパイルが通る
    // 実行時にUnsupportedOperationExceptionが発生する
    unmodified.add("ccc");
}

mutableなインスタンスを保持している場合は、unmodifiableListで生成したリストでも
参照先のmutableなインスタンスが変更されたら変わってしまいます。

@Test
public void unmodifiableListHasMutableTest() throws UnsupportedOperationException {
    final List<MyMutable> list = new ArrayList<>();
    MyMutable myMutable1 = new MyMutable("aaa", 111);
    MyMutable myMutable2 = new MyMutable("bbb", 222);
    list.add(myMutable1);
    list.add(myMutable2);
    final List<MyMutable> unmodified = Collections.unmodifiableList(list);

    // 参照の中身は変更可能
    myMutable1.setMessage("ccc");

    assertThat(unmodified.get(0).getMessage()).isEqualTo("ccc");
}
Guava.ImmutableXXXX

Guavaでもimmutableなコレクションを生成するメソッドが用意されています。
下記のクラスが用意されています。

  • ImmutableCollection
  • ImmutableList
  • ImmutableSet
  • ImmutableSortedSet
  • ImmutableMap
  • ImmutableSortedMap

他にもGuava独自のコレクション(MultisetやBiMapなど)にもImmutableXXXXが用意されています。

java標準のunmodifiableXXXよりも、of、CopyOf、Builderパターンで生成できたりと便利になっているようです。

ImmutableListを使ってListを生成してみます。
add()で要素を追加すると、UnsupportedOperationExceptionが発生します。

@Test(expected = UnsupportedOperationException.class)
public void immutableListTest() throws UnsupportedOperationException {
    final List<String> immutableList = ImmutableList.of("aaa", "bbb");

    final List<String> list = new ArrayList<>();
    list.add("aaa");
    list.add("bbb");
    final List<String> immutableList2 = ImmutableList.copyOf(list);

    // addの操作自体はコンパイルが通る
    // 実行時にUnsupportedOperationExceptionが発生する
    immutableList.add("ccc");
}

mutableなインスタンスを保持している場合は、unmodifiableListで生成したリストでも
参照先のmutableなインスタンスが変更されたら変わってしまいます。

@Test
public void immutableListHasMutableTest() {
    final List<MyMutable> list = new ArrayList<>();
    MyMutable myMutable1 = new MyMutable("aaa", 111);
    MyMutable myMutable2 = new MyMutable("bbb", 222);
    list.add(myMutable1);
    list.add(myMutable2);

    final List<MyMutable> immutableList = ImmutableList.copyOf(list);

    // 参照の中身は変更可能
    myMutable1.setMessage("ccc");

    assertThat(immutableList.get(0).getMessage()).isEqualTo("ccc");
}

サンプルコードは下記にあげました。

https://github.com/pppurple/java_examples/tree/master/multithread_design_pattern/src/main/java/multithread/immutable/fortest
https://github.com/pppurple/java_examples/tree/master/multithread_design_pattern/src/test/java/multithread/immutable

終わり。


【参考】