PR

【Java】シャローコピーとディープコピーを1分で解説

Java

Javaにおける「シャローコピー」と「ディープコピー」について解説します。初心者の方でもイメージしやすいよう、ポイントを押さえつつ、Javaの本質にも触れられるような形で説明していきます。

最初に結論:

  • シャローコピー:オブジェクトのフィールドをコピーするが、参照型は元のオブジェクトと同じインスタンスを共有する。たとえば設定情報や共通データの一時的な複製などで、元データを参照しながら一部だけ加工する用途に向いている。
  • ディープコピー:オブジェクトの中にある参照型も含めてすべて新しいインスタンスとして複製する。トランザクション中のデータバックアップや、変更の影響を完全に切り離した独立処理を行いたい場合に有効。

このページで解説する内容の前提知識

スポンサーリンク

シャローコピー(Shallow Copy)とは?

シャローコピー(表面的なコピー)とは、対象オブジェクトそのものは複製するものの、オブジェクトが保持しているフィールドに関しては参照(アドレス)だけをコピーするやり方を指します。

class Data { String value = "A"; }
Data original = new Data();
Data copy = original; // シャローコピー(参照コピー)
copy.value = "B";
System.out.println(original.value); // → B(元も変わる)

// 📝 同じものを指してるだけなので、片方を変えるともう片方も変わります!

ただ単純に=で代入するだけのやり方で、シャローコピーって何?という人でも無意識のうちに利用できているコピー方法です。

イメージとしては「コピー元の家(オブジェクト)とコピー先の家(オブジェクト)を用意して、家具を別々に用意するのではなく、家具を指し示すラベル(参照)だけをコピー先にも貼り付ける」という感じです。家具そのもの(参照先のインスタンス)はコピーされないため、両方の家で同じ家具を共有している状態になります。

いずれにせよ、こちらは参照型変数の仕組みを理解できていれば自然と使いこなせるコピー方法です。

ポイント シャローコピーの特徴

  • 基本データ型(int, long, boolean など) の値はきちんとコピーされる。
  • オブジェクトの参照型フィールド は “同じインスタンスのアドレス” を共有する。
  • コピー元とコピー先で参照先を共有するため、コピー先から参照先を変更するとコピー元に影響が及ぶ 可能性がある。

シャローコピーするには clone() メソッドが使える

Javaでは、オブジェクトをコピー(複製)したいときに使える便利なメソッドが clone() です。

Q
clone()ってなに?
A

clone() は、今あるオブジェクトとそっくりな新しいオブジェクトを作るためのメソッドです。
ただし、そのコピーは中身までは深くコピーされず、表面だけ(=シャローコピー)になります。

📌 使うときの基本ルール

  1. Cloneable インターフェースを実装すること
  2. clone() メソッドを public にオーバーライドすること
  3. super.clone() を使うこと

Cloneableマーカーインターフェース でメソッドは何も持っていません(=「これを実装したクラスは clone OKですよ」とJavaに伝える目印)。

class Person implements Cloneable {
    // これで「cloneしてもOK」とJavaが認識
}

✅ 具体例(シャローコピー)

class Person implements Cloneable {
    String name;

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone(); // シャローコピーされる
    }
}

public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person p1 = new Person();
        p1.name = "Taro";

        Person p2 = (Person) p1.clone(); // 新しいコピーを作る
        p2.name = "Jiro";

        System.out.println(p1.name); // → Taro(影響なし)
    }
}

🧠 ポイントまとめ

  • clone()シャローコピー用の標準メソッド
  • Cloneable がないと動かない
  • super.clone() を使うことで、簡単にコピーできる
  • ただし参照型のフィールドには注意!(コピー先と元で中身を共有してしまう)

ディープコピー(Deep Copy)とは?

ディープコピー(深いコピー)とは、オブジェクトが保持している参照型のフィールドの先まで再帰的に複製し、まったく独立したオブジェクトを生成するやり方を指します。

class Data { String value = "A"; }

Data original = new Data();
Data copy = new Data();
copy.value = original.value; // ディープコピー(中身だけコピー)

copy.value = "B";
System.out.println(original.value); // → A(元は変わらない)

// 📝 中身を手作業でコピーしているので、コピー後に片方を変えても、もう片方に影響しません!

先ほどの「家」のイメージでいうと、家をコピーする際に、中にある家具も含め、すべて新しく別の物を用意するというイメージです。これなら、片方の家の家具を修理したとしても、もう片方の家には影響しません。

ポイント ディープコピーの特徴

  • オブジェクトが持つすべての参照先を再帰的にコピーする。
  • コピー元とコピー先で同じアドレスを共有しないため、ある箇所を変更してももう一方には影響しない。
  • 実装が複雑になりやすい、あるいはコピーのコストが大きくなる場合がある。

💡 ディープコピーをするための代表的な方法はこの3つ:

方法概要特徴
① コピーコンストラクタnew クラス(元のオブジェクト)で再帰的に中身をコピーする明示的で安全。Java初心者にもおすすめ
clone() メソッドCloneableを実装し、参照型フィールドも clone() で個別にコピーする実装ミスしやすいが、柔軟
③ シリアライズ一度バイト列にして読み直す大規模データやライブラリ使用時に有効。ただし重い

この中で一番わかりやすいのはコピーコンストラクタです!

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

clone() を使う代わりに、コピーコンストラクタ (Copy Constructor) や、あるいはファクトリメソッドでディープコピーを実現する方法もあります。例えば以下のようにできます。

class Person {
    String name;
    int age;
    Address address;

    // コピーコンストラクタ
    public Person(Person other) {
        this.name = other.name;
        this.age = other.age;
        // ここで Address の新しいインスタンスを生成
        this.address = new Address(other.address);
    }
}

class Address {
    String city;
    String street;

    public Address(Address other) {
        this.city = other.city;
        this.street = other.street;
    }
}
  • new Person(元オブジェクト) のように呼び出すことで、新しいPersonインスタンスを作成し、内部のAddressインスタンスなども再帰的に複製します。
  • clone() よりもバグを生みにくい・コードの可読性が良いという意見もあり、実際こちらの方が好まれる場面も多いです。

clone() メソッドを再帰的に呼び出す

先述の Person クラスの場合、シャローコピーだけではなく、参照型フィールドであるAddressについても独自にclone()を呼び出して新インスタンスを作るようにします。例えば下記のようにすることで、addressの実体も新しく複製できます。

class Person implements Cloneable {
    String name;
    int age;
    Address address;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person cloned = (Person) super.clone(); // まずシャローコピー
        // Address も独自にクローン
        if (this.address != null) {
            cloned.address = (Address) this.address.clone();
        }
        return cloned;
    }
}

class Address implements Cloneable {
    String city;
    String street;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
  • Person側でsuper.clone()を使って一度シャローコピーを作成し、次にaddressclone()して新しく生成し、そのインスタンスをcloned.addressに代入します。
  • AddressクラスにもCloneableを実装し、clone()メソッドをオーバーライドする必要があります。

こうすることで、Personをクローンした際に、Addressも新しいインスタンスとしてコピーされるため、それぞれのaddressは別々のものになります。

ライブラリやシリアライズを利用する

  • Apache Commons Lang などのライブラリには、オブジェクトコピー用のユーティリティが用意されている場合があります。
  • シリアライズとデシリアライズObjectOutputStreamObjectInputStreamを利用)を経由してオブジェクトをまるごと複製する方法もあります。
    ただしシリアライズはオーバーヘッドが大きく、またすべてのクラスがシリアライズに対応している(Serializableを実装している)必要があるため、注意が必要です。

シャローコピーとディープコピーを使い分けるポイント

  1. 実装コストとパフォーマンス
    ディープコピーは安全で独立性が高い一方、実装やメモリ・CPUのコストがかかります。小規模なオブジェクトなら問題ない場合が多いですが、非常に複雑なオブジェクト構造を持つ場合、ディープコピーが膨大なリソースを消費しかねません。
  2. 用途・要件に応じて適切な方法を選ぶ
    • もし「コピー元とコピー先を絶対に切り離したい(片方の変更が他方に影響しないようにしたい)」という場合はディープコピーを使います。
    • シンプルな構造、あるいは共有されても困らない部分のみであればシャローコピーでも十分な場合があります。
  3. 実装のわかりやすさ
    clone()を継承チェーン(多くの継承関係)で扱うと混乱しがちです。コピーコンストラクタやファクトリメソッドは実装が明示的でわかりやすい利点があります。

まとめ

  • シャローコピー
    • オブジェクトそのもののフィールドはコピーするが、参照型フィールドの実体は同じものを共有。
    • メモリの負担は軽いが、コピー先とコピー元が干渉し合うリスクがある。
  • ディープコピー
    • 参照型のフィールドを含めて再帰的に複製するため、コピー元とコピー先は完全に独立。
    • メモリ・CPUのコストが増えがちで、実装も複雑になりやすい。

実務では、「どの程度のオブジェクトをどれくらい複製する必要があるのか」「相互の影響をどの程度許容するか」を考慮して、シャローコピーとディープコピーを使い分けます。Javaを利用する上では、コピーの目的やオブジェクトの構造を把握し、適切な方法を選ぶことが大切です。

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