Javaのdeep copy(深コピー)

8890 ワード

この間、Javaオブジェクトを深くコピーする必要がある場合に遭遇しましたが、JDKはdeep copyに関するAPIを提供していません.唯一使えるのは不安定なclone()なので、問題が来ました.安定したdeep copyを実現する方法について、deep copyを実現する方法について説明します.
1.直接賦課
deep copyを実現するには、まず直接値をつけることができるのでしょうか.次のようになります.
    Test test = new Test();
    Test test2 = test;
    
    System.out.println(test);
    System.out.println(test2);

上のコードでは、直接testをtest 2にコピーしますが、2つのオブジェクトを印刷すると、アドレスは実は同じで、testはスタックに割り当てられたばかりのTestオブジェクトの参照にすぎません.ここでの付与値は直接の付与で、test 2もnewが出たばかりのオブジェクトを指しています.ここでのcopyはshallow copyで、copyは1部の参照です.ただし、オブジェクトエンティティにはcopyはありません.値を割り当てることができない以上、2番目のメソッド、Objectクラスのcloneメソッドを試してみましょう.
2.cloneメソッド
1.cloneメソッドの紹介
JavaではすべてのオブジェクトがObjectクラスから継承されるため,cloneメソッドを持つ実装がデフォルトであり,cloneメソッドの実装は比較的単純で乱暴である.まず、オブジェクトがcloneメソッドを呼び出す場合は、Cloneableインタフェースを実装する必要があります.そうしないと、CloneNotSupportedExceptionが放出されます.実はこのCloneableは空のインタフェースで、flagがこのクラスをマークするために使用されているだけでcloneができるので、クラスをCloneableと宣言することはこのクラスがclone能力を持っていることとは直接関係ありません.実はCloneableはコピーという機能を持っていることを示したいので、cloneはCloneableの1つの方法として存在するはずですが、実際にはclone方法はObjectクラスのprotected方法なので、clone方法を直接マルチステートで呼び出すことはできません.例えば:
public class Test implements Cloneable {

    public static void main(String[] args) {
        try {
            List list = new ArrayList();
            Cloneable t1 = new InnerTest("test");
            list.add(t1);
            list.add(t1.clone()); //    ,      
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static class InnerTest implements Cloneable {
        public String a;

        public InnerTest(String test) {
            a = test;
        }
        public Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
}
これは実際には設計上の欠陥であるが、clone方法の評判が悪いのはそれだけではない.
2.cloneは深いコピーか浅いコピーか
cloneメソッドを呼び出すと、まずメモリが直接割り当てられ、元のオブジェクト内のすべてのフィールドが1つずつコピーされます.intなどの基本タイプのデータであれば、直接的な付与式のコピーは問題ありませんが、フィールドが参照であれば問題が発生し、参照も最初の例のようにそのままコピーされます.したがって、多くの場合、cloneは1つの半deep半shallowのコピー方法しか計算できません.この問題を解決するには、コピーするオブジェクトが必要なcloneメソッド内でshallow copyされるオブジェクトのcloneメソッドを呼び出すしかないが、そのオブジェクトもCloneableインタフェースを継承し、Overrideがcloneメソッドを継承していることが前提である.例:
public class Test implements Cloneable {

    public static void main(String[] args) {
        try {
            InnerTest t1 = new InnerTest(new InnerTest2());
            InnerTest t2 = (InnerTest) t1.clone();
            System.out.println(t1); // Test$InnerTest@232204a1
            System.out.println(t2); // Test$InnerTest@4aa298b7
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static class InnerTest implements Cloneable {
        public InnerTest2 test;

        public InnerTest(InnerTest2 test) {
            this.test = test;
        }

        @Override
        public Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }

    public static class InnerTest2 implements Cloneable {
        public InnerTest2() {
        }

        @Override
        public Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
}

3.cloneスキップ構造関数
また、cloneメソッドはコンストラクション関数によって新しいオブジェクトを作成しないため、コンストラクション関数内の論理も直接スキップされ、cloneが制御できないオブジェクトコンストラクションメソッドを導入したことにも問題があります.たとえばコンストラクション関数内でカウント機能を実現したい場合はnewごとに1を加算しますが、cloneの場合、このカウントは有効になりません.例:
public class Test implements Cloneable {

    public static void main(String[] args) {
        try {
            List list = new ArrayList();
            InnerTest t1 = new InnerTest("test");
            InnerTest t2 = new InnerTest("test1");
            list.add(t1);
            list.add(t2);
            list.add((Cloneable) t1.clone());
            for (Cloneable c : list) {
                System.out.println(((InnerTest) c).index ); //      0 1 0
            }
            System.out.println(InnerTest.count); // count 2
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static class InnerTest implements Cloneable {
        public int index;
        public static int count = 0;

        public InnerTest(String test) {
            index = count;
            count++;
        }
        public Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
}

4.ベストプラクティス--コンストラクション関数をコピーしたり、Copyableインタフェースをカスタマイズしたりします.また、cloneメソッド自体もスレッドが安全ではありません.だからまとめるとcloneは頼りにならないので、主流の提案はやはり複製構造関数を追加することです.これは面倒ですが、制御性が強く、deep copyを実現することができます.
また、独自にCopyインタフェースを実装し、コピーしたいクラスがそのインタフェースを継承し、copy関数を復元することもできます.しかしcopy関数内の論理はコピー構造と似ている.例:
Copyインタフェース:
public interface Copyable {
    T copy ();
}

具体的な実装とテスト:
public class Test{
    public static void main(String[] args) {
        try {
            InnerTest t1 = new InnerTest(new InnerTest2());
            InnerTest t2 = t1.copy();
            System.out.println(t1.test.getA()); // print 0
            t1.test.setA(5);
            System.out.println(t2.test.getA()); // print 0
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //    
    public static class InnerTest implements Copyable {
        // set to public for convenience
        public InnerTest2 test;

        public InnerTest(InnerTest2 tmp) {
            this.test = tmp;
        }

        @Override
        public InnerTest copy() {
            InnerTest2 tmp = test == null ? null : test.copy();
            return new InnerTest(tmp);
        }
    }

    //    ,  getter setter     
    public static class InnerTest2 implements Copyable{
        private int a;
        public InnerTest2() {
            a = 0;
        }

        public int getA() {
            return a;
        }

        public void setA(int a) {
            this.a = a;
        }

        @Override
        public InnerTest2 copy() {
            InnerTest2 tmp = new InnerTest2();
            tmp.setA(this.a);
            return tmp;
        }
    }
}

3.シーケンス化による深いレプリケーション
1.シーケンス化を使用する理由
ほとんどの場合、レプリケーション構造は良い選択ですが、実装上は確かに煩雑で、エラーが発生しやすいです.すべてのオブジェクトとその参照オブジェクトを再帰的にレプリケーションする必要があるため、deep copyを実現する別の考え方があります.Java Object Serialization(JOS)です.シーケンス化は、親、フィールド、および参照を含むオブジェクトのさまざまな側面を考慮します.したがって、1つのオブジェクトをバイトストリームにシーケンス化してから読み出し、オブジェクトに再構築すれば、このオブジェクトのdeep copyを実現することができる.もちろん,ここでは構造関数論理も考慮していないが,shallow copyの可能性を考慮する必要はなく,煩雑な複製構造やcopy法の上書きを省き,deepCopy関数を1つ実現することでオブジェクト複製を直接実現できる.この方法についてご紹介します.
2.深いレプリケーションの実装
deepCopy関数を実装する方法については、次の簡単な例を示します.
public class Test2 {
    public static Object deepCopy(Object from) {
        Object obj = null;
        try {
            //       Byte Array
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);
            out.writeObject(from);
            out.flush();
            out.close();

            //       byte array,  readObject         
            ObjectInputStream in = new ObjectInputStream(
                    new ByteArrayInputStream(bos.toByteArray()));
            obj = in.readObject();
        } catch(IOException e) {
            e.printStackTrace();
        } catch(ClassNotFoundException e2) {
            e2.printStackTrace();
        }
        return obj;
    }
}
上記の例では、deepCopy関数を呼び出すことで、1つのオブジェクトをdeep copyし、新しいオブジェクトを返すことができます.ここでwriteObjectとreadObjectはそれぞれオブジェクトをシーケンス化し逆シーケンス化する.
3.シーケンス化の問題点
この方法は簡単に見えますが、実際には多くの問題があります.
まず,シーケンス化を実現するにはシーケンス化インタフェースを実現しなければならない,すなわち,深く複製する必要があるすべてのクラスがSerializableインタフェースを実現すべきであることを示すが,これは比較的容易に解決できる.
第二に、シーケンス化の操作は比較的に遅く、実際にはシーケンス化と逆シーケンス化の2つの操作は比較的に時間がかかり、これは自分でwriteObjectとreadObjectを実現することで解決することができるが、ここでは常にボトルネックである.
第三に、シーケンス化操作ではByteArrayInputStreamとByteArrayOutputStreamはスレッドが安全であり、一般的には問題ないが、自身の業務でマルチスレッドに関与しない場合はdeep copyの速度が遅くなる.
ここで、第2の実装は面倒であり、速度の向上は明らかではないが、マルチスレッドに関与しない場合、第3の実装は変更され、非スレッドで安全なInputStreamとOutputStreamのサブクラスがByteArrayInputStreamとByteArrayOutputStreamに置き換えられ、速度を向上させることができる.
4.関連サードパーティライブラリの使用
前に述べたいくつかの案はそれぞれ長所と短所があり,実現が煩雑であるか,機能が不安定であるかのいずれかである.一般的にこの時,関連機能のある成熟したクラスライブラリがあるかどうかを見ることができるが,deep copyに関するサードパーティライブラリはDozer(https://github.com/DozerMapper/dozer),Kryo(https://github.com/EsotericSoftware/kryo),cloning(https://github.com/kostaskougios/cloning)など、成熟したクラスライブラリを使用するとdeep copyを迅速かつ効率的に実現でき、具体的な配布はここでは言うまでもなくgithub上のドキュメントを直接見ればよい.
まとめて、deep copyを実現するには、主な方法は次のとおりです.
  • Cloneableインタフェースを実装しcloneメソッド
  • を上書き
  • 複製コンストラクタ
  • を使用
  • Copyableインタフェースをカスタマイズし、cloneを必要とするクラスにcopyメソッドを追加する具体的な実装
  • シーケンス化により1つのオブジェクトを先にシーケンス化してから逆シーケンス化してdeep copyの新しいオブジェクト
  • を得る.
  • 成熟したサードパーティライブラリを使用して、具体的な方法はドキュメントを参照してください.