Java | オブジェクト指向:defensive copy

Java Java
スポンサーリンク

defensive copy とは何か

defensive copy(防御的コピー)は
「オブジェクトの中身を、外から勝手に壊されないように、防御のためにコピーを取るテクニック」
のことです。

特に ListDate など“中身を書き換えられるオブジェクト”をフィールドに持つときに重要になります。

外から渡されたオブジェクトをそのまま保持してしまうと、
呼び出し側があとから中身を変更したときに、自分の内部状態まで勝手に変わってしまいます。
それを防ぐために、「渡されたものをそのまま持たず、一度コピーしてから持つ」のが defensive copy です。


まずは「やばいコード」を見てみる

外から渡されたリストを、そのまま持ってしまう例

注文クラスを考えます。

final class Order {

    private final List<String> items;

    Order(List<String> items) {
        this.items = items;   // そのまま代入
    }

    List<String> items() {
        return items;         // そのまま返している
    }
}
Java

一見シンプルですが、呼び出し側からこう使われたら危険です。

List<String> list = new ArrayList<>();
list.add("Apple");
Order order = new Order(list);

list.add("Banana");           // ← あとからリストを変更
System.out.println(order.items());   // [Apple, Banana] が出てしまう
Java

Order は「Apple だけの注文」のつもりで作ったのに、
外のコードが list.add("Banana") したことで、
Order の中身まで勝手に変わってしまいました。

Order の内部状態が、「自分の知らない場所のコード」で簡単に壊されてしまう。
これが defensive copy をしないことで起きる典型的な問題です。


defensive copy の基本:コンストラクタでコピーする(重要)

渡されたものをそのまま持たない

先ほどの Order を defensive copy を使って直してみます。

final class Order {

    private final List<String> items;

    Order(List<String> items) {
        if (items == null || items.isEmpty()) {
            throw new IllegalArgumentException("商品は1件以上必要です");
        }
        this.items = new ArrayList<>(items);   // defensive copy!
    }

    List<String> items() {
        return new ArrayList<>(items);         // ここもコピーして返す
    }
}
Java

これで、さっきと同じコードをもう一度動かしてみます。

List<String> list = new ArrayList<>();
list.add("Apple");
Order order = new Order(list);

list.add("Banana");   // ここで外側の list は変わる

System.out.println(order.items());  
// [Apple] ← Order の中身は変わらない
Java

コンストラクタの中で new ArrayList<>(items) しているので、
Order は自分だけのリストを持っています。
外の list をどれだけいじっても、Order の中は影響を受けません。

これが defensive copy の基本パターンです。


getter でも defensive copy が必要になる理由

内部のリストをそのまま返すとどうなるか

先ほどの最初の実装では、getter も危険でした。

List<String> items() {
    return items;  // そのまま返してしまう
}
Java

呼び出し側でこう書けてしまいます。

Order order = ...;
order.items().clear();      // 外から中身を全部消せてしまう
Java

Order の内部リストへの「生の参照」を渡してしまっているので、
呼び出し側がそれを通じて中身をいじり放題になります。

getter でコピーを返す

これを防ぐのが、getter 側の defensive copy です。

List<String> items() {
    return new ArrayList<>(items);   // コピーを返す
}
Java

こうしておけば、

Order order = ...;
List<String> view = order.items();

view.clear();                 // view は空になる
System.out.println(order.items());  // でも order の中身は無事
Java

呼び出し側が受け取ったリストをどういじっても、
Order の中に閉じ込めたリストには影響しません。

「内部状態を守るために、外との間にコピーを挟む」のが defensive copy の考え方です。


Date でも同じ問題が起きる(古典的な例)

java.util.Date の罠

java.util.Date は可変(あとから setTime で値を書き換えられる)なので、
これをそのままフィールドに持つと同じ問題が起きます。

final class Event {

    private final Date when;

    Event(Date when) {
        this.when = when;         // そのまま代入
    }

    Date when() {
        return when;              // そのまま返す
    }
}
Java

呼び出し側でこうすると:

Date d = new Date();
Event e = new Event(d);

d.setTime(0L);                   // 外から書き換え
System.out.println(e.when());    // Event の日時も変わってしまう
Java

これも defensive copy で防げます。

Date 用の defensive copy

final class Event {

    private final Date when;

    Event(Date when) {
        if (when == null) {
            throw new IllegalArgumentException("日時は必須");
        }
        this.when = new Date(when.getTime());    // defensive copy
    }

    Date when() {
        return new Date(when.getTime());         // defensive copy して返す
    }
}
Java

こうすると、外から setTime でいじられても
Event の中の日時は変わりませんし、
getter で返したオブジェクトをいじっても影響しません。


いつ defensive copy が必要で、いつは不要か(深掘り)

必要なとき

defensive copy を考えるべきなのは、ざっくり言うと次のようなときです。

フィールドに代入しようとしているオブジェクトが「可変」である
フィールドを通じて、不変条件や内部状態を壊されたくない
getter で内部のオブジェクトを外に渡す必要がある

具体的には、

List / Set / Map
java.util.Date / Calendar
配列(int[]String[] など)

など「中身を書き換え可能なもの」を扱うときは、
「これ、そのまま持って大丈夫か?」と一度立ち止まって考えるとよいです。

不要なとき

逆に、次のような場合は defensive copy は不要です。

String のような完全なイミュータブル(中身を書き換えられない)
Integer, LocalDate, LocalDateTime, 自作のイミュータブル値オブジェクト

これらは、一度作られたら二度と中身が変わらないので、
そのままフィールドに持っても、getter で返しても安全です。

final class Person {

    private final String name;        // String はイミュータブル
    private final LocalDate birthday; // これもイミュータブル

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

    String name() { return name; }
    LocalDate birthday() { return birthday; }  // defensive copy 不要
}
Java

この違いを理解しておくと、
「どこで defensive copy を使うべきか」が自然に見えてきます。


defensive copy が守っているもの(設計面での意義)

不変条件とカプセル化の保護

オブジェクト指向の重要な考え方の一つに、
「オブジェクトは自分の一貫性を自分で守る」というものがあります。

defensive copy をしないと、

コンストラクタで「ちゃんとした状態」にしたつもりでも
あとから外部コードに勝手に書き換えられて、一貫性が壊れる

ということが簡単に起きてしまいます。

defensive copy は、
「外の世界」と「自分の内部状態」のあいだにクッションを挟むことで、
カプセル化と不変条件を守っているわけです。

バグの原因が「勝手に変わった」に見えなくなるのを防ぐ

defensive copy がないと、
「誰かがどこかでリストを書き換えたせいで、気づいたら中身が変わっていた」
という、原因追跡が非常にしんどいバグが生まれます。

defensive copy をしておけば、

自分のクラスのメソッド経由でしか中身は変わらない
変えるときは必ず、自分のルールに従って変わる

という状態にできます。

「どこからでも何となく変わる」状態と、
「このメソッドを通してしか変わらない」状態では、
デバッグの難易度が天と地ほど違います。


まとめ:defensive copy を使うか迷ったら考えること

defensive copy は、

外から渡された可変オブジェクトを
そのまま内部に持たず、コピーを取っておくことで
内部状態を他人に壊されないようにするテクニック

です。

迷ったときは、

このフィールドのオブジェクトは、あとから中身を変えられるタイプか?
このフィールドが勝手に変わると、クラスの不変条件は壊れないか?
getter で渡した相手から、中身をいじってほしくないか?

あたりを自分に問いかけてみてください。

タイトルとURLをコピーしました