Java | Java 標準ライブラリ:clone の注意点

Java Java
スポンサーリンク

まず「clone は基本おすすめされない」という前提から

いきなり結論から言うと、
Object#clone() は「あるけど、できれば使わない方がいい API」です。

理由はシンプルで、

挙動が分かりにくい(浅いコピー/深いコピー問題)
正しく実装するのが意外と難しい
バグが出ても気づきにくい

からです。

とはいえ、標準ライブラリには clone() が存在するし、
既存コードでも目にします。
だから「なぜ注意すべきなのか」「使うならどこに気をつけるか」を、
初心者向けに整理しておきます。


clone の基本仕様をざっくり押さえる

Object#clone は protected で、Cloneable が必要

Object クラスには、こういうメソッドがあります。

protected native Object clone() throws CloneNotSupportedException;
Java

ポイントは

アクセス修飾子が protected(そのままでは外から呼べない)
戻り値が Object
CloneNotSupportedException が throws される

ということです。

クラスが「クローンして OK ですよ」という意思表示をするには、

クラスが Cloneable インターフェースを実装する
clone()public にオーバーライドする

の2ステップが必要になります。


典型的な clone 実装パターンと「浅いコピー」

浅いコピーとは何か

よく見る定番の実装です。

public class User implements Cloneable {

    private String name;
    private int age;

    @Override
    public User clone() {
        try {
            return (User) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e); // 起きない想定のラップ
        }
    }
}
Java

super.clone() は「フィールドをビット単位でコピーするだけ」です。
これを「浅いコピー(shallow copy)」と呼びます。

プリミティブ型(int や boolean)は「値」がそのままコピーされます。
参照型(String や自前のクラス)は、「参照」がコピーされるだけです。

つまり、オブジェクトの中に別のオブジェクトを持っている場合、
クローン元とクローン先で「中身のオブジェクトを共有する」ことになります。

浅いコピーが問題になる例(重要)

例えば、User が List を持っている場合。

public class User implements Cloneable {

    private String name;
    private List<String> tags;

    public User(String name, List<String> tags) {
        this.name = name;
        this.tags = tags;
    }

    @Override
    public User clone() {
        try {
            return (User) super.clone();  // 浅いコピー
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }

    public List<String> tags() {
        return tags;
    }
}
Java

これを使ってみます。

List<String> tags = new ArrayList<>();
tags.add("java");
User u1 = new User("Taro", tags);
User u2 = u1.clone();

u2.tags().add("spring");

System.out.println(u1.tags()); // ?
System.out.println(u2.tags()); // ?
Java

「clone したのだから、u1 と u2 は独立」と思いきや、
実際には

u1.tags() も u2.tags() も、同じ List インスタンスを指している

ので、u2 側の変更が u1 にも見えてしまいます。

これが「浅いコピーの罠」です。
見た目は別インスタンスなのに、中で状態を共有してしまう。


深いコピーをしようとして地獄を見るパターン

全部手でコピーしないといけない

浅いコピーが危ないと感じて「じゃあ深いコピー(deep copy)にしよう」と思うと、
今度は実装がどんどん重くなります。

深いコピーとは、「中に持っているオブジェクトまで含めて、全部新しいインスタンスにする」ことです。

さっきの User なら、こんな感じになります。

@Override
public User clone() {
    try {
        User copy = (User) super.clone();
        copy.tags = new ArrayList<>(this.tags);  // List も新しく作る
        return copy;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError(e);
    }
}
Java

この程度ならまだマシですが、
フィールドが増えるほど、
さらにその中に複雑なオブジェクトを持つほど、
深いコピーの実装は泥沼になります。

「どこまでコピーすべきか」をすべてのフィールドで意識しないといけなくなり、
ちょっとした変更で「コピー漏れ」が発生しがちです。

継承を絡めるとさらに難しくなる

clone()Object の protected メソッドを継承していくスタイルなので、
継承階層が深くなると、スーパークラス・サブクラスが互いに clone 実装を意識する必要が出てきます。

スーパークラス側が

super.clone() を呼ぶかどうか
内部状態をどう初期化するか

を意識しないと、サブクラス側の clone が壊れます。

この「継承と clone の組み合わせ」が、
Effective Java(有名な Java 本)で「clone は避けろ」と言われる大きな理由の一つです。


Cloneable インターフェースの微妙さ

「マーカーインターフェース」という古い設計

Cloneable インターフェースは、メソッドを一切持ちません。

public interface Cloneable {
}
Java

そのかわり、「このインターフェースを実装しているかどうか」で
Object#clone() が成功するか、CloneNotSupportedException を投げるかが決まります。

これは「マーカーインターフェース」と呼ばれる古い設計パターンで、
今の視点から見ると

コンパイル時に何も保証してくれない
実装しているだけでは clone メソッドがないこともある

など、使い勝手が悪いです。

つまり、Cloneable を implements しただけでは

「本当に clone をサポートしているのか」
「深いコピーなのか浅いコピーなのか」

がコードからは全然見えません。


実務的には「clone 以外の手段」を選びがち

コピーコンストラクタやファクトリメソッドを使う

今どきの Java でオブジェクトを複製したいときは、
clone() よりも

コピーコンストラクタ
コピー用の static メソッド

などを使うことが多いです。

例えばさっきの User を、clone を使わずにコピーするなら:

public class User {

    private final String name;
    private final List<String> tags;

    public User(String name, List<String> tags) {
        this.name = name;
        this.tags = new ArrayList<>(tags); // ここでコピー
    }

    // コピーコンストラクタ
    public User(User other) {
        this(other.name, other.tags); // コンストラクタを再利用
    }

    public static User copyOf(User other) {
        return new User(other);
    }
}
Java

こうしておけば:

new User(user)
User.copyOf(user)

のように「明示的にコピーを作る」ことができます。

clone と違って

例外の扱いがシンプル
継承との絡みが少ない
浅いコピー/深いコピーの挙動を自分で設計しやすい

というメリットがあります。

不変オブジェクトなら「そもそもコピー不要」にできる

さらに一歩進むと、「不変オブジェクト(immutable)」にする、という考え方があります。

フィールドを全部 final にして、
コンストラクタ以外で状態を変えられないようにしたクラスです。

public final class Money {

    private final int amount;

    public Money(int amount) {
        if (amount < 0) throw new IllegalArgumentException();
        this.amount = amount;
    }

    public int amount() {
        return amount;
    }

    public Money add(Money other) {
        return new Money(this.amount + other.amount);
    }
}
Java

この Money は一度作ったら中身が変わりません。
なので「コピーする必要」がほとんどなく、

「同じ値を持つ別インスタンス」が欲しければ new すればいい
そもそも、参照を共有しても問題ない

という状態になります。

「コピーしたい」という欲求の多くは、「後から中身を変えたい」から来ています。
ならば、中身を変えられない設計に寄せることで、
clone 自体を使わずに済ませるのが、オブジェクト指向的にはきれいです。


初心者が clone で特に気をつけるべきポイント

どんなときに危ないかを整理する

clone を使うときに、特に気をつけるべきなのは次のような状況です。

フィールドに List や Map、配列、他のオブジェクトを持っている
そのフィールドの中身を、後から変更する可能性がある
clone したオブジェクトと元のオブジェクトを「独立して」扱いたい

この条件が揃っているのに、浅いコピーの clone を使うと、
後から「なんで元のオブジェクトまで変わるの?」という事故になりがちです。

また、

継承関係があるクラス階層で clone を使う
外部ライブラリのクラスをサブクラス化して clone をいじる

といったケースも、習熟してからでないと危険です。

「clone を見たら、一回立ち止まる」くらいでちょうどいい

初心者向けの現実的なアドバイスとしては、

自分で新しく設計するクラスに clone は使わない
既存コードで clone を見かけたら、「浅いか深いか」「何を共有しているか」を確認する

というスタンスがちょうどいいです。

どうしても clone を使わざるを得ない場面(既存設計との互換性、
古いライブラリの仕様など)が出てきたら、そのときに
具体的なクラスを一緒に見ながら慎重に扱う、で十分です。


まとめ:clone の注意点を一言で言うと

clone()

デフォルトで「浅いコピー」を行うだけ
複雑なオブジェクト構造ではバグの温床になりやすい
継承やミュータブルなフィールドと組み合わせるとさらに危険

という性質を持っています。

だからこそ、

新しいコードではコピーコンストラクタやファクトリメソッドで複製する
できるだけ不変オブジェクト設計に寄せて、そもそもコピーを減らす
既存の clone 実装に触るときは「何が共有されているか」を必ず確認する

この3つを意識しておくと、clone に振り回されにくくなります。

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