はじめに
Javaで参照型オブジェクトを複製するとき、変数へ代入するだけでは同じ参照を共有します。コピー先の変更がコピー元へ伝わるため、初心者は参照とインスタンスの違いを意識する必要があります。
ディープコピーは、オブジェクト本体と内部の参照先を別オブジェクトとして複製する実装です。Javaではclone()、Serializable、コピーコンストラクタ、ライブラリ利用のどれでも、共有したくない参照を新しく作る点が基本です。
最小のプログラムでシャローコピーとの差を見てから、ArrayList、HashMap、カスタムクラスへ広げると理解しやすくなるのが基本です。JavaのList操作はJava List型完全ガイドも参考になります。
動作確認環境、理論、サンプルコード、応用、注意点の順に、Javaディープコピーの実装判断を整理します。出力例は期待される結果として読み、==や片方だけの変更で参照共有の有無を確認するのが目安です。
- Java SE 21 / JDK 21 APIを基準に記述
- Apache Commons Lang 3系の
SerializationUtilsを例示
- シャローコピーとディープコピーの違い
clone()とCloneableを使う基本実装Serializableによるコピーの作り方- リスト、マップ、カスタムオブジェクトへの応用
- 循環参照や例外処理などの注意点
Javaとは
Javaは、classで型を定義し、newでインスタンスを作るオブジェクト指向言語です。変数には多くの場合オブジェクトへの参照が入り、コピー処理では値の複製か参照共有かを分けて考えます。
JavaのStringやIntegerのような不変オブジェクトは共有しやすい一方、ArrayListや自作クラスは可変状態を持つため注意が必要です。JVMのガーベッジコレクションは不要オブジェクトを回収しますが、参照共有の設計ミスまでは防ぎません。
Javaの特徴
Javaの特徴は、プラットフォーム独立性、オブジェクト指向、自動メモリ管理、標準ライブラリの広さです。ディープコピーではObject、Cloneable、Serializableなどを組み合わせて方針を決めます。
ただし、すべてのクラスを同じ方法で複製できるわけではありません。Thread、Socket、ファイルハンドルなど外部リソースと結び付くオブジェクトは、Javaのディープコピー対象から外す判断もあります。
| 対象 | 代表API | 向く場面 | 注意点 |
|---|---|---|---|
| 単純な値 | String / int | そのまま代入 | 不変性を確認 |
| 参照を持つクラス | clone() | 階層が浅いモデル | 参照型フィールドを個別に複製 |
| 直列化できるクラス | ObjectOutputStream | 構造をまとめて複製 | Serializableが必要 |
| コレクション | List / Map | 要素ごとのコピー | 要素が可変なら要素も複製 |
| 循環参照 | IdentityHashMap | グラフ構造 | 訪問済み管理が必要 |
| 外部ライブラリ | SerializationUtils | 定型処理の削減 | 依存関係と性能を確認 |
これらの特徴は、コピー処理そのものを保証しません。同じAddressを複数のPersonが参照しているかは、Javaのメモリ管理ではなく実装側が判断します。
モデルクラスでは可変性を小さくする設計も有効です。コンストラクタで値を受け取り、外部へ可変な参照を直接返さないようにすると、ディープコピーが必要な箇所を減らせます。
ディープコピーの必要性
ディープコピーが必要になるのは、元データを保ったまま別処理で値を変更したい場面です。画面入力を一時編集するプログラムでは、確定前の変更が元データへ反映されると取消処理が難しくなるのがポイントです。
編集用オブジェクト、履歴管理、テストデータ生成、並行処理の境界では、コピー元とコピー先を分離します。Javaアノテーションの12選で扱うメタ情報付きクラスでも、内部フィールドの扱いが実装方針を左右します。
注文、明細、配送先のような親子関係では、親だけをコピーしても明細リストを共有していれば変更が混ざりますし、ここがポイントです。一方、国コードや税率区分のような参照マスタは共有できる場合があるため、変更可能性と所有関係からコピー範囲を決めます。
Javaにおけるディープコピーの理論
Javaのディープコピーは、コピー対象のオブジェクトグラフをどこまで複製するかを決める作業です。変更可能な参照先を共有しないことが中心で、不変値や共有してよいリソースは例外として扱います。
公式ドキュメントでは、Object.clone()がフィールド単位のコピーを行う操作として説明されているのが一般的です。super.clone()だけでは参照型フィールドの中身まで新しくならないため、追加の実装が必要です。
ディープコピーとは
ディープコピーとは、参照型フィールドにも新しいオブジェクトを作成し、コピー元とコピー先を独立して変更できる状態にする処理です。nameだけなら差は見えにくくても、AddressやListを持つと違いが明確になります。
どこまで深くコピーするかは要件で決まります。共有すべきマスタデータまで複製すると、メモリ使用量や整合性管理が重くなるためです。
編集画面の一時保存、Undo機能、リクエスト間のデータ受け渡しでは、変更の境界を明確にすると不具合を追いやすくなるのが現実的です。Javaでは「どこで値が変わるか」「誰が所有するか」「共有してよい寿命か」を基準にディープコピーを選びます。
シャローコピーとディープコピーの違い
シャローコピーは、オブジェクト自身のフィールド値を写しますが、参照型フィールドでは参照値を共有します。ディープコピーは参照先のオブジェクトも別に作るため、片方の変更がもう片方へ伝わりにくくなると整理できます。
違いはperson.address.cityのようなネストした値を変更したときに表れます。シャローコピーなら元のaddressも変わり、ディープコピーならコピー先のaddressだけが変わります。
💡 Tips: 不変オブジェクトは共有しても変更されないため、ディープコピー対象から外す設計があると覚えるとよいでしょう。可変オブジェクトだけを新規作成すると、コピー処理の負担を抑えられますが、これは押さえたい点です。
List内の要素が自作クラスの場合、リスト本体と要素の両方を分けて考えます。レビュー時は「コンテナだけを作り直していないか」「要素まで複製しているか」「不変値を過剰に作り直していないか」を確認します。
Javaでのディープコピーの実装方法概観
Javaでの実装方法は、clone()、コピーコンストラクタ、Serializableによる直列化、外部ライブラリに分けられますし、これが一つの目安です。初心者には、フィールドを明示して新しいオブジェクトを作るコピーコンストラクタやdeepCopy()メソッドが読みやすいです。
シリアライゼーションは構造全体を扱えますが、対象クラスがSerializableを満たす必要があります。OracleのSerializable APIドキュメントでも、実装クラスだけがシリアライズ対象になることが示されています。
コピーコンストラクタは型ごとの意図を読みやすく、シリアライゼーションは対象が広いほど記述量を減らせますが、覚えておくと役立つでしょう。ライブラリは定型処理を短縮できますが、依存関係、例外、対象外フィールドの扱いを理解してから選びます。
clone()は言語機能として存在していても、可読性や設計方針で好みが分かれます。新規設計ならcopy()やコピーコンストラクタを選び、既存コードとの整合が必要な場合だけCloneableに寄せる判断もあると理解できます。
Javaディープコピーの基本的な実装法
Javaディープコピーの基本は、クラスが持つ参照型フィールドを洗い出すことです。clone()を使う場合でも、addressのようなフィールドはthis.address.clone()で個別に複製します。
コピー処理をクラス内部へ閉じ込めると、呼び出し側のプログラムはperson.deepCopy()やperson.clone()を呼ぶだけで済みます。フィールド追加時の修正箇所も見つけやすくなると覚えるとよいでしょう。
cloneメソッドを用いた実装
clone()を使う実装では、コピー対象クラスがCloneableを実装し、CloneNotSupportedExceptionを扱います。Cloneableを付けずにObject.clone()を呼ぶと例外につながります。
super.clone()はシャローコピーに近いため、参照型フィールドを再代入する処理が必要です。初心者はこの1行を忘れやすく、内部状態だけ共有される問題が残りますし、ここを基本と考えるとよいでしょう。
サンプルコード1:基本的なcloneメソッドを用いたディープコピー
このサンプルコードは、PersonがAddressを持つ構造で、Person.clone()内からAddress.clone()を呼びます。コピー後にperson2.address.cityを変えても、person1.address.cityは元の値を保ちます。
結果: 期待される出力は、コピー元がTaro, Tokyo、コピー先がJiro, Osakaになる内容です。
結果: 期待される出力例では、person2の変更がperson1へ伝わっていないことを読み取れます。
Addressが文字列フィールドだけを持つため、この例ではsuper.clone()で足ります。さらに可変オブジェクトが増えた場合は、そのフィールドも深く複製するカスタマイズが必要です。
シリアライゼーションを用いた実装
シリアライゼーションを使う実装では、オブジェクトをバイト列に変換し、そのバイト列から新しいオブジェクトを復元すると考えられます。到達可能なオブジェクトグラフをまとめて複製しやすい方法です。
対象クラスと内部の参照先がSerializableを満たさない場合、NotSerializableExceptionが発生します。Javaエスケープ処理の10ステップマスターガイドの文字列処理とは異なり、オブジェクト単位の状態を扱う点に注意します。
サンプルコード3:シリアライゼーションを用いたディープコピー
このサンプルコードは、ByteArrayOutputStreamとObjectOutputStreamでPersonをバイト列化し、ByteArrayInputStreamとObjectInputStreamで復元すると言えるでしょう。実装を整えるならtry-with-resourcesを使います。
結果: 期待される出力は、コピー元とコピー先が同じフィールド値を持つ内容です。
結果: 期待される出力例では値が同じに見えますが、復元されたcopiedは別インスタンスとして扱われます。
この方法は全フィールドが直列化可能であることに依存します。外部リソースや一時的な状態を持つフィールドにはtransientを検討し、復元後の整合性も設計するのが基本です。
サンプルコード4:外部ライブラリを利用したシリアライゼーションによるディープコピー
Apache Commons LangのSerializationUtils.clone()を使うと、直列化を利用したJavaディープコピーの定型コードを短くできます。公式のSerializationUtils APIドキュメントでも、シリアライゼーションによるディープクローン用途が示されています。
結果: 期待される出力は、SerializationUtils.clone(original)で作ったコピーが元と同じ値を持つ内容です。
結果: 期待される出力例では同じ文字列表現になりますが、copiedはシリアライズとデシリアライズを経た別オブジェクトです。
ライブラリ利用は万能ではありません。対象クラスがSerializableでなければ使えず、コピーコストもオブジェクトグラフの大きさに左右されます。
ディープコピーの実例
ディープコピーの実例は、オブジェクト、リスト、マップの順に見ると整理しやすくなります。どのデータ構造でも、共有してよいものと共有してはいけないものを分けますし、ここがポイントです。
判断は業務ロジックだけでなく、テストのしやすさにも影響します。日付判定のような値中心の処理では、Javaでうるう年を判定する解説のように入力と出力を分け、可変状態を減らす設計も候補です。
特に重要なのは、コンテナと要素を分ける視点です。new ArrayList<>()でリスト本体を作っても、中の要素が同じなら完全な分離ではありません。
オブジェクトの複製
オブジェクトの複製では、コピーコンストラクタや専用のdeepCopy()メソッドを用意すると意図が明確です。Personの中にAddressがあるなら、参照先もnew Address(address.street, address.city)で作ります。
サンプルコード5:複雑なオブジェクトのディープコピー
このサンプルコードでは、コンストラクタ内でAddressを作り直し、deepCopy()から新しいPersonを返します。コピー処理の意図だけが見える形です。
結果: 期待される出力は、コピー元とコピー先が同じ住所値を持つ表示です。
結果: 期待される出力例では値が同じですが、Person生成時にAddressを作り直す点がディープコピーの核になります。
この形式は、フィールド追加時にコピー対象を明示しやすい実装です。単体テストではコピー後に一方だけを変更するケースを置き、共有漏れを確認します。
リストの複製
リストの複製では、リスト本体だけをnew ArrayList<>(originalList)で作っても、要素が可変オブジェクトなら共有が残りますが、これは押さえたい点です。要素が不変か可変かを見て、必要なら要素ごとのディープコピーを実装します。
サンプルコード6:ArrayListのディープコピー
このサンプルコードは、originalListからcopiedListへ要素を追加し、コピー後に先頭要素を書き換えます。Stringは不変ですが、可変要素ならitem.deepCopy()のような処理に置き換えますし、これが一つの目安です。
結果: 期待される出力は、コピー先の先頭だけがGrapeへ変わる内容です。
結果: 期待される出力例では、copiedList.set(0, "Grape")がoriginalListへ影響していません。
new String(item)は学習用の表現で、通常のJavaコードでは不変なStringを作り直す必要は薄いです。可変な要素型へ応用する場合は、要素クラス側にコピーコンストラクタを用意します。
マップの複製
マップの複製では、キーと値を別々に考えます。キーも値も不変なら新しいHashMapで足りる場合がありますが、値が可変オブジェクトなら値側のディープコピーが必要です。
サンプルコード7:HashMapのディープコピー
このサンプルコードは、entrySet()を走査して別のHashMapへ要素を移するのが目安です。new Integer(int)は現行Javaでは推奨されないため、Integer.valueOf()を使います。
結果: 期待される出力は、copiedMapのOneだけが11へ変わる内容です。
結果: 期待される出力例では、originalMap側のOneは1のままです。
HashMapの表示順は仕様上固定ではないため、環境によってキーの並びが変わる可能性があります。独立性を見るときは、順序ではなくキーに対応する値を確認します。
ディープコピーの応用とカスタマイズ
ディープコピーの応用では、対象クラスの性質に合わせてコピー範囲をカスタマイズするのがポイントです。すべてを深く複製するだけでなく、IDやマスタ参照は共有し、編集対象の値だけを分離する設計もあります。
こうした調整はプログラムの性能や保守性にも関係します。Javaのオーバーライド解説で触れるメソッド再定義と同じく、コピー処理をどのクラスに持たせるかを決めると変更に強くなるのが一般的です。
コピー後の状態も設計対象です。新規登録扱いなら識別子を空にし、履歴コピーなら作成者や作成日時を保持するなど、用途に応じてcopyForEdit()、copyForHistory()のようにメソッドを分けると混乱を減らせます。
カスタムオブジェクトのディープコピー
カスタムオブジェクトでは、Java標準APIだけでなくドメインルールをコピー処理へ反映できます。たとえばidはコピーしない、createdAtは引き継ぐ、itemsは要素ごとに複製する、といったカスタマイズです。
サンプルコード8:カスタムオブジェクトのディープコピーのカスタマイズ
このサンプルコードは、Serializableを使ってCustomObject自身をバイト列化し、復元結果を返するのが現実的です。例外を呼び出し側へ渡すため、利用側でIOExceptionとClassNotFoundExceptionを扱います。
結果: 期待される動きは、deepCopy()が同じフィールド値を持つ別のCustomObjectを返すことです。
この実装は、内部フィールドが増えても直列化対象であればコピー処理を書き足さずに済みます。一方、serialVersionUID、transient、互換性の設計が必要になるため、長期保存用の直列化とメモリ上のコピー用途を混同しないほうが安全です。
Serializableによるコピーは、すべての参照先がシリアライズ可能であることに依存します。対象外のフィールドを含む場合は、コピーコンストラクタや専用メソッドへ切り替える判断が必要です。パフォーマンス最適化
ディープコピーは、対象のオブジェクトグラフが大きいほど処理量が増えますが、覚えておくと役立つでしょう。シリアライゼーション方式は分かりやすい反面、バイト列への変換と復元を挟むため、常に最速とは言えません。
性能を意識する場面では、必要なフィールドだけをコピーする手動実装、変更不能オブジェクトの活用、コピー回数の削減を検討します。Javaプログラムではデータ量と呼び出し回数も見ます。
サンプルコード9:パフォーマンス最適化を行ったディープコピーの実装
このサンプルコードは、nullを先に返し、シリアライズ可能なオブジェクトだけをSerializationUtils.clone()へ渡す汎用メソッドです。名称は最適化ですが、定型処理の共通化が中心です。
結果: 期待される動きは、objがnullならnullを返し、シリアライズ可能なら複製した値を返すことです。
この形にすると、呼び出し側はSerializableでない型を渡しにくくなります。ただし、例外を握りつぶしてnullを返す設計は原因の把握を遅らせるため、独自例外へ包むか呼び出し側へ伝えます。
ディープコピーのトラブルシューティング
トラブルシューティングでは、例外の種類とコピー対象の構造を分けて調べますし、ここを基本と考えるとよいでしょう。CloneNotSupportedExceptionならCloneable、NotSerializableExceptionならSerializable、循環参照なら訪問済み管理を確認します。
printStackTrace()だけで終わらせると、呼び出し側が失敗を判断しにくくなります。学習用のサンプルコードでは原因表示に使えますが、アプリケーションではログ出力と例外伝播の方針を決めますし、ここがポイントです。
サンプルコード10:トラブルシューティングの例
このサンプルコードは、CustomObject.deepCopy()が例外を投げる可能性をtryとcatchで扱います。初心者は、コピー対象と参照先が直列化できるかを確認します。
結果: 期待される動きは、コピーに成功すれば例外が出ず、失敗時にはスタックトレースが標準エラーへ出ることです。
copiedObjectを作るだけでは画面やコンソールへ値は表示されません。動作を観察したい場合はtoString()を実装し、System.out.println(copiedObject)を追加します。
例外を握りつぶして処理を続けると、コピーに失敗したnullが後続処理へ渡り、原因から離れた場所でNullPointerExceptionになることがあります。ログには対象クラス名や操作名を残し、個人情報や機密値は出さないようにすると整理できます。
注意点と対処法
Javaでディープコピーを実装するときの注意点は、例外、コピー漏れ、性能、循環参照の4系統です。実装前に対象クラスのフィールド一覧を確認するのが基本です。
確認では、privateフィールド、継承元の状態、コレクション内の要素、staticフィールドを分けます。staticは通常コピー対象外で、finalフィールドは設計によってコピー方法が制限されます。
レビューでは出力例だけでなく、コピー後の参照関係を確認すると理解できます。単体テストでは、コピー元を変えたケースとコピー先を変えたケースの両方を置くと、共有漏れを検出しやすくなります。
一般的なエラーとその対処法
一般的なエラーは、CloneNotSupportedException、NotSerializableException、ClassCastException、コピー後の共有残りです。エラー名だけでなく、どのフィールドで発生したかを追います。
CloneNotSupportedException
CloneNotSupportedExceptionは、Object.clone()の利用時にCloneableを満たしていない場合に起きますが、これは押さえたい点です。clone()をpublicにするだけでは足りず、クラス宣言にimplements Cloneableを付けます。
対処法
コピー対象クラスにimplements Cloneableを追加し、super.clone()の戻り値を適切な型へキャストします。その後、参照型フィールドにnewや各フィールドのclone()を使い、共有を残さないようにすると覚えるとよいでしょう。
シャローコピーとディープコピーの混同
シャローコピーとディープコピーの混同は、super.clone()だけでコピーが完了したと考えると発生します。内部のListやAddressを共有していれば、コピー後の変更が元へ波及します。
対処法
コピー後に片方だけの参照型フィールドを変更し、もう片方へ影響しないか確認するテストを置きますし、これが一つの目安です。たとえばcopied.address.city = "Osaka"の後で、original.address.cityがTokyoのままなら分離できています。
recordや不変クラスの設計は、ディープコピーの複雑さを下げる選択肢になるのが基本です。パフォーマンスへの影響と最適化
パフォーマンスへの影響は、コピー対象の大きさ、コピー頻度、方式で変わりますが、覚えておくと役立つでしょう。深いオブジェクトグラフを毎回シリアライズすると、処理時間とメモリ使用量が増える場合があります。
手動コピーは必要なフィールドだけを選べるため、性能面では有利になることがあります。一方、フィールド追加時にコピー漏れが起きるため、テストとレビューを組み合わせますし、ここを基本と考えるとよいでしょう。
効率的なディープコピー
効率的なディープコピーでは、すべてを深く複製する前に共有しても安全な値を見極めます。String、LocalDate、BigDecimalのような不変に近い値は共有し、ArrayListや可変な自作クラスを新しく作ります。
対処法
コピー対象を「不変値」「可変値」「外部リソース」に分け、可変値だけを深く複製すると考えられます。外部リソースはコピーせず、接続の再生成や参照共有など別のライフサイクル管理を選びます。
キャッシング
キャッシングは、循環参照を持つグラフ構造のコピーで役立ちます。親子が互いを参照する構造では、何も管理せず再帰コピーすると同じオブジェクトを何度もたどりますし、ここがポイントです。
対処法
IdentityHashMapのような訪問済みマップに、元オブジェクトとコピー済みオブジェクトの対応を保存します。再訪問時は新しく作らずキャッシュから返すため、循環参照と重複コピーを避けられます。
キャッシュを使う実装はコード量が増えますが、これは押さえたい点です。木構造で循環がないと分かっているなら単純な再帰コピーで足りる場合もあるため、データ構造の制約を先に明文化します。
コピー対象を減らす設計も有効です。値を変更する処理を局所化し、読み取り専用のオブジェクトを多く保てば、Javaディープコピーを呼ぶ場面を減らせます。
設計段階ではコピー方式だけでなく、可変状態をどこに置くかも検討すると言えるでしょう。可変状態を小さなクラスへ閉じ込めると、サンプルコードから本格的なプログラムへ広げるときも影響範囲を追いやすくなります。
ディープコピーは単なる複製処理ではなく、変更を閉じ込めるための設計手段です。基本を押さえたうえで応用やカスタマイズへ進むと、注意点にも気づきやすくなります。
大量データを扱う処理では、必要なタイミングだけ複製し、読み取り専用で足りる場面では共有を残するのが基本です。変更される値だけを追えば、Java実装の見通しと保守時の確認範囲を小さくできます。
まとめ
Javaのディープコピーは、参照型フィールドを共有しないように別オブジェクトへ複製する実装です。clone()、コピーコンストラクタ、Serializable、外部ライブラリのどれを選んでも、可変な参照先の扱いが基本になります。
初心者はサンプルコードを写すだけでなく、コピー後に片方を変更する確認を入れると理解しやすくなるのが目安です。PersonとAddress、List、Mapの例は、プログラム内の共有状態を見抜く練習になります。
応用では、不変値は共有し、可変値は新しく作り、外部リソースは別管理にします。カスタマイズの余地を残す場合は、クラスごとにdeepCopy()やコピーコンストラクタを用意すると保守しやすくなるのがポイントです。
注意点は、CloneNotSupportedException、NotSerializableException、シャローコピーの混入、循環参照です。これらを整理してから実装すれば、Javaのディープコピーはデータ保護や編集処理の基盤として扱いやすくなります。
関連記事
- Java List型完全ガイド!初心者でもマスターできる7つのステップ
- Javaアノテーションの12選!初心者から上級者まで徹底ガイド
- Javaでうるう年を判定!初心者でも分かる9ステップ解説
- Javaエスケープ処理の10ステップマスターガイド
- Javaでマスターする!オーバーライドのたった7つのステップ
※本記事は実在のエンジニア複数名で構成される Japanシーモア編集部が、AI支援を活用して作成・校正・公開しています。


