16
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【Java】不変オブジェクトの作り方 参照の隠蔽と防衛的コピー

Last updated at Posted at 2016-09-29

前回の記事で、Javaにおける不変オブジェクトのメリットを簡単に説明しました。

前回はStringやjava.timeパッケージ内の時間クラスなど、JavaのAPIとして用意されているクラスを使って説明しましたが、今度は自作クラスを不変オブジェクトにするための工夫について説明してみます。

参照の隠蔽

まず大事なのは「参照を隠す」ということです。
言葉にすると難しそうですが、要はインスタンス変数をprivateにして、不必要なsetterは定義しないようにしましょうね、ということです。

以下は、 Person.java という、参照の隠蔽を実践したクラスです。

Person.java
public class Person {
  private String name;

  public Person(String name) {
    this.name = name;
  }
}

このPersonクラスを使うメインクラスは ImmutableSample.java とします。

ImmutableSample.java
public class ImmutableTest {
  public static void main(String args[]) {
    String name = "Taro";
    Person taro = new Person(name);
  }
}

Personクラスの使用者であるImmutableTestクラスは、Personクラスの name フィールドにアクセスする術を持たず、またname変数もStringクラスがそもそも不変であるため、taro インスタンスの保持する値( name 変数に格納された "Taro" という文字列)を変更することはできません。

インスタンス変数がString型だけであれば、これで不変クラスは完成です。

防衛的コピー

では次に、先ほどのPersonクラスに「誕生日」を表すDate型の birthday 変数を追加してみます。

Person.java
public class Person {
  private String name;
  private Date birthday;

  public Person(String name, Date birthday) {
    this.name = name;
    this.birthday = birthday;
  }

  public String toString() {
    return String.format("%sさんの誕生日は%sです。", name, new SimpleDateFormat("yyyy/MM/dd").format(birthday));
  }
}

先ほどの「参照の隠蔽」で説明した通り、このクラスはインスタンス変数が全て private でsetterメソッドも定義されていません。しかし、このクラスは不変ではありません。以下のように使うことで、birthday を書き換えることができるためです。

ImmutableTest.java
public class ImmutableTest {
  public static void main(String args[]) throws ParseException {
    // Taroインスタンスの生成
    String name = "Taro";
    Date birthday = new SimpleDateFormat("yyyyMMdd").parse("19870929");
    Person taro = new Person(name, birthday);

    System.out.println(taro.toString());

    // birthdayを外から変更
    birthday.setTime(System.currentTimeMillis());
    System.out.println(taro.toString());
  }
}
Taroさんの誕生日は1987/09/29です。
Taroさんの誕生日は2016/09/29です。

これでは、mainメソッドの birthday 変数が持つ参照値があとの処理に引き回されでもしたら、いつTaroの誕生日が変わってしまうか予測できない状況になってしまいます。

これを防ぐための工夫が「防衛的コピー」です。Personクラスを以下のように書き換えます。

Person.java
public class Person {
  private String name;
  private Date birthday;

  public Person(String name, Date birthday) {
    this.name = name;
    this.birthday = new Date(birthday.getTime()); // birthdayインスタンスのコピーを保持する
  }

  public String toString() {
    return String.format("%sさんの誕生日は%sです。", name, new SimpleDateFormat("yyyy/MM/dd").format(birthday));
  }
}

この状態で、ImmutableTest.javaのソースコードはそのまま変えずに実行すると、

Taroさんの誕生日は1987/09/29です。
Taroさんの誕生日は1987/09/29です。

となり、いくら外からTaroの持つ変数を書き換えようとしてもできない状態を作り出すことができます。

また多くの場合、Taroの誕生日を参照するためのgetterメソッドを用意する必要がります。このとき単純にgetterメソッドを用意してしまうと、せっかくコンストラクタでコピーした birthday の参照値を使用者に渡してしまうことになります。

Person.java
public Date getBirthday() {
  return birthday;
}

このような場合も防衛的コピーを使います。

Person.java
public Date getBirthday() {
  return new Date(birthday.getTime());
}

これで、不変でない Date 型のフィールドを持つPersonクラスを不変にすることができました。

注)
日付クラスについてはJava8から不変性をもつクラスがいくつも追加されていますので、実際は「そもそもDateクラスを使わない」というのが正しいのですが、今回は説明のためにあえてDateクラスを使用しています。

リストを持つクラスを不変にする

最後に、Personに休暇日を表す vacations を追加してみます。

Person.java
public class Person {
  private String name;
  private Date birthday;
  private List<Date> vacations;

  public Person(String name, Date birthday, List<Date> vacations) {
    this.name = name;
    this.birthday = new Date(birthday.getTime());
    this.vacations = new ArrayList<>(vacations);
  }

  public List<Date> getVacations() {
    return new ArrayList<>(vacations);
  }
}

birthday と同じように防衛的コピーを使ってみました。これで大丈夫でしょうか。

ImmutableTest.java
public class ImmutableTest {
  public static void main(String args[]) throws ParseException {

    SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd");

    String name = "Taro";
    Date birthday = format.parse("19870929");
    List<Date> vacations = new ArrayList<>();
    vacations.add(format.parse("20161003"));
    vacations.add(format.parse("20161005"));
    vacations.add(format.parse("20161010"));
    Person taro = new Person(name, birthday, vacations);

    SimpleDateFormat printFormat = new SimpleDateFormat("yyyy/MM/dd");
    for (Date vacation : taro.getVacations()) {
      System.out.println(printFormat.format(vacation));
    }

    taro.getVacations().get(0).setTime(System.currentTimeMillis());

    for (Date vacation : taro.getVacations()) {
      System.out.println(printFormat.format(vacation));
    }
  }
}
2016/10/03
2016/10/05
2016/10/10

2016/09/29 //書き換えられた!
2016/10/05
2016/10/10

書き換えられてしまいました。
taro インスタンスの vacations インスタンス自体は不変ですが、その中の要素はImmutableTest.java の vacations 変数が持つ要素と同じインスタンスであるため、このままでは外から書き換えることが可能です。

それを防ぐためには、リスト内の要素についても防衛的コピーを適用する必要があります。
また、getterメソッドも同様にします。

Person.java
public class Person {
  private String name;
  private Date birthday;
  private List<Date> vacations;

  public Person(String name, Date birthday, List<Date> vacations) {
    this.name = name;
    this.birthday = new Date(birthday.getTime());

    // リストを新しく作り、中の要素についてもひとつずつ防衛的コピーをする。
    this.vacations = new ArrayList<>();
    for (Date vacation : vacations) {
      this.vacations.add(new Date(vacation.getTime()));
    }
  }

  public List<Date> getVacations() {
    // インスタンスのvacationsインスタンスだけでなく、中の要素もひとつずつ防衛的コピーをしてから返却する。
    List<Date> vacations = new ArrayList<>();
    for (Date vacation : this.vacations) {
      vacations.add(new Date(vacation.getTime()));
    }
    return vacations;
  }
}

ImmutableTest.java はそのまま変えずに実行すると、以下のようになります。

2016/10/03
2016/10/05
2016/10/10

2016/10/03 //書き換えられない!
2016/10/05
2016/10/10

これで、リストを渡す場合でも不変性を実現することができました。

まとめ

ここまで、オブジェクトを不変にするための工夫を紹介してきましたが、実際はもしかしたら完全に不変なオブジェクトを作るという機会はあまりないかもしれません。開発者がお互いにコミュニケーションをとれる状況なのであれば、不用意にインスタンスの内容を書き換えない、参照値を引き回さない、といった「使う側」のコーディングで対処できる場合があるからです。また、防衛的コピーは不変性を実現する一方で、getするたびに new が呼ばれてしまう、というデメリットもあります。

Effective Javaにも繰り返し書かれているのですが、このようなテクニックが常に正しいとは限りません。要件や状況に応じて使い分ける必要があります。

とはいえ、Javaで他のクラスにインスタンス(の参照値)を渡すことによってどのような危険性が生じるのかを知る、という意味で、不変オブジェクトの考え方を知っておくのはとても良い勉強になるかと思います。

16
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?