Javaのヒント76:ディープコピー手法の代替

オブジェクトのディープコピーを実装することは、学習体験になる可能性があります-あなたはそれをしたくないことを学びます!問題のオブジェクトが他の複雑なオブジェクトを参照し、そのオブジェクトが他のオブジェクトを参照している場合、このタスクは確かに困難な場合があります。従来、オブジェクト内の各クラスは、Cloneableインターフェイスを実装し、そのclone()メソッドをオーバーライドして、それ自体とそれに含まれるオブジェクトのディープコピーを作成するために、個別に検査および編集する必要があります。この記事では、この時間のかかる従来のディープコピーの代わりに使用する簡単な手法について説明します。

ディープコピーの概念

ディープコピーとは何かを理解するために、まず浅いコピーの概念を見てみましょう。

以前のJavaWorldの記事「トラップを回避し、java.lang.Objectのメソッドを正しくオーバーライドする方法」で、Mark Rouloは、オブジェクトのクローンを作成する方法と、ディープコピーではなくシャローコピーを実現する方法について説明しています。ここで簡単に要約すると、浅いコピーは、オブジェクトが含まれているオブジェクトなしでコピーされるときに発生します。図1に示すオブジェクトを説明するためにobj1、それは、2つのオブジェクトが含まれ、containedObj1そしてcontainedObj2

obj1図2に示すように、で浅いコピーが実行されると、コピーされますが、含まれているオブジェクトはコピーされません。

ディープコピーは、オブジェクトが参照先のオブジェクトと一緒にコピーされるときに発生します。図3はobj1、ディープコピーが実行された後を示しています。obj1コピーされただけでなく、そこに含まれるオブジェクトもコピーされました。

これらの含まれているオブジェクトのいずれかにオブジェクトが含まれている場合、ディープコピーでは、グラフ全体がトラバースされてコピーされるまで、それらのオブジェクトもコピーされます。各オブジェクトは、そのclone()メソッドを介して自身のクローンを作成する責任があります。clone()から継承されたデフォルトのメソッドObjectは、オブジェクトの浅いコピーを作成します。ディープコピーを実現するには、含まれているすべてのオブジェクトのclone()メソッドを明示的に呼び出すロジックを追加する必要があります。このロジックは、含まれているオブジェクトのclone()メソッドを呼び出します。これを正しく行うことは困難で時間がかかる可能性があり、楽しいことはめったにありません。さらに複雑にするために、オブジェクトを直接変更できず、そのclone()メソッドが浅いコピーを生成する場合は、クラスを拡張する必要があります。clone()メソッドがオーバーライドされ、この新しいクラスが古いクラスの代わりに使用されます。(たとえばVector、ディープコピーに必要なロジックは含まれていません。)また、オブジェクトをディープコピーにするかシャローコピーにするかという問題を実行時まで延期するコードを記述したい場合は、さらに複雑になります。状況。この場合、オブジェクトごとに2つのコピー関数が必要です。1つはディープコピー用、もう1つはシャローコピー用です。最後に、ディープコピーされるオブジェクトに別のオブジェクトへの複数の参照が含まれている場合でも、後者のオブジェクトは1回だけコピーする必要があります。これにより、オブジェクトの拡散が防止され、循環参照がコピーの無限ループを生成するという特別な状況が回避されます。

シリアル化

1998年1月、JavaWorldは、Mark JohnsonによるJavaBeansコラムを、シリアル化に関する記事「フリーズドライされたJavaBeansを使用して「ネスカフェ」の方法で実行する」で開始しました。要約すると、シリアル化とは、オブジェクトのグラフ(単一のオブジェクトの縮退した場合を含む)を、同等のオブジェクトのグラフに戻すことができるバイトの配列に変換する機能です。オブジェクトまたはその祖先の1つがまたはを実装しているjava.io.Serializable場合、そのオブジェクトはシリアル化可能であると言われますjava.io.Externalizable。シリアル化可能writeObject()ObjectOutputStreamオブジェクトは、オブジェクトのメソッドに渡すことでシリアル化できます。これにより、オブジェクトのプリミティブデータ型、配列、文​​字列、およびその他のオブジェクト参照が書き出されます。ザ・writeObject()次に、参照されたオブジェクトに対してメソッドが呼び出され、それらもシリアル化されます。さらに、これらの各オブジェクトには参照とオブジェクトがシリアル化されています。このプロセスは、グラフ全体がトラバースされてシリアル化されるまで続きます。これはおなじみですか?この機能を使用して、ディープコピーを作成できます。

シリアル化を使用したディープコピー

シリアル化を使用してディープコピーを作成する手順は次のとおりです。

  1. オブジェクトのグラフ内のすべてのクラスがシリアル化可能であることを確認してください。

  2. 入力ストリームと出力ストリームを作成します。

  3. 入力ストリームと出力ストリームを使用して、オブジェクト入力ストリームとオブジェクト出力ストリームを作成します。

  4. コピーするオブジェクトをオブジェクト出力ストリームに渡します。

  5. オブジェクト入力ストリームから新しいオブジェクトを読み取り、送信したオブジェクトのクラスにキャストし直します。

ObjectClonerステップ2から5を実装するというクラスを作成しました。 「A」とマークされた行ByteArrayOutputStreamObjectOutputStream、オンラインBを作成するために使用されるaを設定します。行Cは、魔法が行われる場所です。このwriteObject()メソッドは、オブジェクトのグラフを再帰的にトラバースし、バイト形式で新しいオブジェクトを生成して、それをに送信しますByteArrayOutputStream。行Dは、オブジェクト全体が送信されたことを確認します。次に、行Eのコードは、ByteArrayInputStreamを作成し、その内容を入力しByteArrayOutputStreamます。行Fは、行Eで作成されたObjectInputStreamを使用してインスタンス化しByteArrayInputStream、オブジェクトは逆シリアル化され、行Gの呼び出しメソッドに返されます。コードは次のとおりです。

インポートjava.io. *;インポートjava.util。*; importjava.awt。*; public class ObjectCloner {//誰も誤ってObjectClonerオブジェクトを作成できないようにするprivateObjectCloner(){} //オブジェクトのディープコピーを返すstaticpublic Object deepCopy(Object oldObj)throws Exception {ObjectOutputStream oos = null; ObjectInputStream ois = null; {ByteArrayOutputStream bos = new ByteArrayOutputStream();を試してください。 // A oos = new ObjectOutputStream(bos); // B //オブジェクトをシリアル化して渡しますoos.writeObject(oldObj); // C oos.flush(); // D ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray()); // E ois = new ObjectInputStream(bin); // F //新しいオブジェクトを返しますreturnois.readObject(); // G} catch(Exception e){System.out.println( "Exception in ObjectCloner =" + e); throw(e); }最後に{oos.close(); ois.close(); }}}

ObjectClonerこのコードを実行する前に、にアクセスできるすべての開発者は、オブジェクトのグラフ内のすべてのクラスがシリアル化可能であることを確認する必要があります。ほとんどの場合、これはすでに行われているはずです。そうでない場合は、ソースコードへのアクセスを使用して比較的簡単に実行できるはずです。JDKのほとんどのクラスはシリアライズ可能です。など、プラットフォームに依存するものだけFileDescriptorがそうではありません。また、JavaBeanに準拠しているサードパーティベンダーから取得したクラスは、定義上、シリアル化可能です。もちろん、シリアライズ可能なクラスを拡張すると、新しいクラスもシリアライズ可能になります。これらのシリアル化可能なクラスがすべて浮かんでいるので、シリアル化する必要があるのは自分のものだけである可能性があります。これは、各クラスを通過して上書きするのに比べれば簡単です。clone() ディープコピーを実行します。

オブジェクトのグラフにシリアル化できないクラスがあるかどうかを確認する簡単な方法は、それらがすべてシリアル化可能であると想定し、その上でObjectClonerdeepCopy()メソッドを実行することです。クラスがシリアル化できないオブジェクトがある場合は、ajava.io.NotSerializableExceptionがスローされ、問題の原因となったクラスが示されます。

簡単な実装例を以下に示します。それは単純なオブジェクト、作成v1され、Vector含まれていることをPoint。次に、このオブジェクトが印刷され、その内容が表示されます。次に、元のオブジェクト、v1が新しいオブジェクト、にコピーされますvNew。このオブジェクトは、と同じ値が含まれていることを示すために印刷されv1ます。次に、の内容をv1変更し、最後に両方v1vNewを出力して、値を比較できるようにします。

インポートjava.util。*; importjava.awt。*; public class Driver1 {static public void main(String [] args){try {//コマンドラインからメソッドを取得しますStringmeth; if((args.length == 1)&&((args [0] .equals( "deep"))||(args [0] .equals( "shallow")))){meth = args [0]; } else {System.out.println( "使用法:java Driver1 [深い、浅い]");戻る; } //元のオブジェクトを作成するVectorv1 = new Vector();ポイントp1 = new Point(1,1); v1.addElement(p1); //それが何であるかを確認しますSystem.out.println( "Original =" + v1);ベクトルvNew = null; if(meth.equals( "deep")){//ディープコピーvNew =(Vector)(ObjectCloner.deepCopy(v1)); // A} else if(meth.equals( "shallow")){//浅いコピーvNew =(Vector)v1.clone(); // B} //同じであることを確認しますSystem.out.println( "New =" + vNew);//元のオブジェクトの内容を変更しますp1.x = 2; p1.y = 2; //それぞれに何が含まれているかを確認しますSystem.out.println( "Original =" + v1); System.out.println( "New =" + vNew); } catch(Exception e){System.out.println( "Exception in main =" + e); }}}

ディープコピー(行A)を呼び出すには、を実行しjava.exe Driver1 deepます。ディープコピーを実行すると、次の出力が得られます。

オリジナル= [java.awt.Point [x = 1、y = 1]]新規= [java.awt.Point [x = 1、y = 1]]オリジナル= [java.awt.Point [x = 2、y = 2]]新規= [java.awt.Point [x = 1、y = 1]] 

これは、元のPointp1が変更Pointされた場合、グラフ全体がコピーされたため、ディープコピーの結果として作成された新しいものは影響を受けないことを示しています。比較のために、を実行してシャローコピー(行B)を呼び出しjava.exe Driver1 shallowます。浅いコピーを実行すると、次のプリントアウトが得られます。

オリジナル= [java.awt.Point [x = 1、y = 1]]新規= [java.awt.Point [x = 1、y = 1]]オリジナル= [java.awt.Point [x = 2、y = 2]]新規= [java.awt.Point [x = 2、y = 2]] 

This shows that when the original Point was changed, the new Point was changed as well. This is due to the fact that the shallow copy makes copies only of the references, and not of the objects to which they refer. This is a very simple example, but I think it illustrates the, um, point.

Implementation issues

Now that I've preached about all of the virtues of deep copy using serialization, let's look at some things to watch out for.

The first problematic case is a class that is not serializable and that cannot be edited. This could happen, for example, if you're using a third-party class that doesn't come with the source code. In this case you can extend it, make the extended class implement Serializable, add any (or all) necessary constructors that just call the associated superconstructor, and use this new class everywhere you did the old one (here is an example of this).

This may seem like a lot of work, but, unless the original class's clone() method implements deep copy, you will be doing something similar in order to override its clone() method anyway.

The next issue is the runtime speed of this technique. As you can imagine, creating a socket, serializing an object, passing it through the socket, and then deserializing it is slow compared to calling methods in existing objects. Here is some source code that measures the time it takes to do both deep copy methods (via serialization and clone()) on some simple classes, and produces benchmarks for different numbers of iterations. The results, shown in milliseconds, are in the table below:

Milliseconds to deep copy a simple class graph n times
Procedure\Iterations(n) 1000 10000 100000
clone 10 101 791
serialization 1832 11346 107725

As you can see, there is a large difference in performance. If the code you are writing is performance-critical, then you may have to bite the bullet and hand-code a deep copy. If you have a complex graph and are given one day to implement a deep copy, and the code will be run as a batch job at one in the morning on Sundays, then this technique gives you another option to consider.

Another issue is dealing with the case of a class whose objects' instances within a virtual machine must be controlled. This is a special case of the Singleton pattern, in which a class has only one object within a VM. As discussed above, when you serialize an object, you create a totally new object that will not be unique. To get around this default behavior you can use the readResolve() method to force the stream to return an appropriate object rather than the one that was serialized. In this particular case, the appropriate object is the same one that was serialized. Here is an example of how to implement the readResolve() method. You can find out more about readResolve() as well as other serialization details at Sun's Web site dedicated to the Java Object Serialization Specification (see Resources).

One last gotcha to watch out for is the case of transient variables. If a variable is marked as transient, then it will not be serialized, and therefore it and its graph will not be copied. Instead, the value of the transient variable in the new object will be the Java language defaults (null, false, and zero). There will be no compiletime or runtime errors, which can result in behavior that is hard to debug. Just being aware of this can save a lot of time.

The deep copy technique can save a programmer many hours of work but can cause the problems described above. As always, be sure to weigh the advantages and disadvantages before deciding which method to use.

Conclusion

複雑なオブジェクトグラフのディープコピーを実装することは、困難な作業になる可能性があります。上に示したclone()手法は、グラフ内のすべてのオブジェクトのメソッドを上書きする従来の手順の簡単な代替手段です。

Dave Millerは、コンサルティング会社Javelin Technologyのシニアアーキテクトであり、Javaおよびインターネットアプリケーションに取り組んでいます。彼は、Hughes、IBM、Nortel、MCIWorldcomなどの企業でオブジェクト指向プロジェクトに携わっており、過去3年間はJavaのみを使用してきました。

このトピックの詳細

  • SunのJavaWebサイトには、Java Object SerializationSpecification専用のセクションがあります。

    //www.javasoft.com/products/jdk/1.2/docs/guide/serialization/spec/serialTOC.doc.html

このストーリー「JavaTip76:ディープコピー手法の代替」は、もともとJavaWorldによって公開されました。