Petua Java 76: Alternatif untuk teknik salin dalam

Melaksanakan salinan mendalam objek dapat menjadi pengalaman belajar - anda mengetahui bahawa anda tidak mahu melakukannya! Sekiranya objek yang dimaksud merujuk pada objek kompleks lain, yang pada gilirannya merujuk kepada objek lain, maka tugas ini memang menakutkan. Secara tradisinya, setiap kelas dalam objek mesti diperiksa dan diedit secara individu untuk melaksanakan Cloneableantara muka dan mengesampingkan metodenya clone()untuk membuat salinan mendalam serta objek yang terdapat di dalamnya. Artikel ini menerangkan teknik mudah untuk digunakan sebagai pengganti salinan dalam konvensional yang memakan masa ini.

Konsep salinan dalam

Untuk memahami apa itu salinan dalam , mari kita lihat konsep penyalinan cetek.

Dalam artikel JavaWorld sebelumnya , "Bagaimana untuk mengelakkan perangkap dan mengganti kaedah dengan betul dari java.lang.Object," Mark Roulo menerangkan bagaimana mengklon objek dan juga bagaimana untuk membuat penyalinan cetek dan bukannya menyalin mendalam. Untuk meringkaskan secara ringkas di sini, salinan cetek berlaku apabila objek disalin tanpa objek yang terkandung. Untuk menggambarkan, Gambar 1 menunjukkan sebuah objek obj1, yang berisi dua objek, containedObj1dan containedObj2.

Sekiranya salinan cetek dilakukan obj1, maka salinannya akan disalin tetapi objek yang terdapat di dalamnya tidak seperti yang ditunjukkan pada Gambar 2

Salinan mendalam berlaku apabila objek disalin bersama dengan objek yang merujuknya. Gambar 3 menunjukkan obj1setelah salinan mendalam dilakukan di atasnya. Tidak hanya obj1telah disalin, tetapi benda-benda yang ada di dalamnya telah disalin juga.

Sekiranya salah satu objek terkandung itu sendiri mengandungi objek, maka, dalam salinan mendalam, objek tersebut juga disalin, dan seterusnya sehingga keseluruhan graf dilintasi dan disalin. Setiap objek bertanggungjawab untuk pengklonan sendiri melalui clone()kaedahnya. clone()Kaedah lalai , diwarisi dari Object, membuat salinan objek yang cetek. Untuk mencapai salinan mendalam, logik tambahan mesti ditambahkan yang secara eksplisit memanggil semua clone()kaedah objek terkandung , yang seterusnya memanggil clone()kaedah objek terkandungnya , dan sebagainya. Mendapatkan ini betul boleh menjadi sukar dan memakan masa, dan jarang menyenangkan. Untuk membuat perkara menjadi lebih rumit, jika objek tidak dapat diubah secara langsung dan clone()kaedahnya menghasilkan salinan cetek, maka kelas mesti diperpanjang,clone()kaedah diganti, dan kelas baru ini digunakan sebagai ganti yang lama. (Contohnya, Vectortidak mengandungi logik yang diperlukan untuk salinan dalam.) Dan jika anda ingin menulis kod yang membendung sehingga waktu runtuh, persoalan sama ada membuat salinan objek dalam atau cetek, anda akan lebih rumit keadaan. Dalam kes ini, mesti ada dua fungsi salinan untuk setiap objek: satu untuk salinan dalam dan satu untuk salinan cetek. Akhirnya, walaupun objek yang disalin mendalam mengandungi banyak rujukan ke objek lain, objek yang terakhir harus disalin sekali sahaja. Ini menghalang percambahan objek, dan mengatasi situasi khas di mana rujukan pekeliling menghasilkan gelung salinan yang tidak terhingga.

Serialisasi

Kembali pada bulan Januari 1998, JavaWorld memulakan kolom JavaBeans oleh Mark Johnson dengan sebuah artikel mengenai serialisasi, "Lakukan dengan cara 'Nescafé' - dengan JavaBeans yang telah dibekukan." Ringkasnya, serialisasi adalah kemampuan untuk mengubah grafik objek (termasuk kasus degenerasi dari satu objek) menjadi array byte yang dapat diubah kembali menjadi grafik objek yang setara. Objek dikatakan boleh disenaraikan jika ia atau salah satu nenek moyangnya melaksanakan java.io.Serializableatau java.io.Externalizable. Objek yang boleh disenaraikan dapat diselaraskan dengan menyebarkannya ke writeObject()kaedah ObjectOutputStreamobjek. Ini menuliskan jenis data primitif objek, tatasusunan, rentetan, dan rujukan objek lain. ThewriteObject()kaedah kemudian dipanggil pada objek yang dirujuk untuk menyusunnya juga. Selanjutnya, setiap objek mempunyai mereka rujukan dan objek bersiri; proses ini berterusan dan berterusan sehingga keseluruhan graf dilintasi dan bersiri. Adakah ini terdengar biasa? Fungsi ini dapat digunakan untuk mencapai salinan mendalam.

Salinan mendalam menggunakan siri

Langkah-langkah untuk membuat salinan dalam menggunakan serialisasi adalah:

  1. Pastikan semua kelas dalam grafik objek boleh dibuat siri.

  2. Buat aliran input dan output.

  3. Gunakan aliran input dan output untuk membuat input objek dan aliran output objek.

  4. Lulus objek yang ingin anda salin ke aliran output objek.

  5. Baca objek baru dari aliran input objek dan hantar kembali ke kelas objek yang anda hantar.

Saya telah menulis kelas yang dipanggil ObjectCloneryang melaksanakan langkah dua hingga lima. Garis bertanda "A" menetapkan a ByteArrayOutputStreamyang digunakan untuk membuat ObjectOutputStreamgaris B. Garis C adalah tempat keajaiban dilakukan. The writeObject()kaedah rekursif merentasi graf objek, menjana objek baru dalam bentuk bait, dan menghantarnya kepada ByteArrayOutputStream. Baris D memastikan keseluruhan objek telah dihantar. Kod pada baris E kemudian membuat a ByteArrayInputStreamdan mengisi dengan isi ByteArrayOutputStream. Baris F menunjukkan ObjectInputStreampenggunaan yang ByteArrayInputStreamdibuat pada baris E dan objek tersebut terdeseralisasikan dan dikembalikan ke kaedah panggilan pada baris G. Inilah kodnya:

import java.io. *; import java.util. *; import java.awt. *; ObjectCloner kelas awam {// supaya tidak ada yang boleh membuat objek ObjectCloner peribadi ObjectCloner secara tidak sengaja () {} // mengembalikan salinan mendalam objek objek statik awam DeepCopy (Object oldObj) melemparkan Pengecualian {ObjectOutputStream oos = null; ObjectInputStream ois = null; cuba {ByteArrayOutputStream bos = ByteArrayOutputStream baru (); // A oos = ObjectOutputStream baru (bos); // B // bersiri dan lulus objek oos.writeObject (oldObj); // C oos.flush (); // D ByteArrayInputStream bin = ByteArrayInputStream baru (bos.toByteArray ()); // E ois = ObjectInputStream baru (tong sampah); // F // kembalikan objek baru kembali ois.readObject (); // G} tangkapan (Pengecualian e) {System.out.println ("Pengecualian dalam ObjectCloner =" + e); baling (e); } akhirnya {oos.close (); ois.close (); }}}

Semua pembangun yang mempunyai akses harus ObjectClonerdilakukan sebelum menjalankan kod ini memastikan bahawa semua kelas dalam grafik objek boleh disirikan. Dalam kebanyakan kes, ini sudah seharusnya dilakukan; jika tidak, semestinya mudah dilakukan dengan mengakses kod sumber. Sebilangan besar kelas di JDK adalah bersiri; hanya yang tidak bergantung pada platform, seperti FileDescriptor. Juga, mana-mana kelas yang anda dapat dari vendor pihak ketiga yang mematuhi JavaBean secara definisi boleh diselaraskan. Sudah tentu, jika anda memanjangkan kelas yang boleh disenaraikan, maka kelas yang baru juga boleh diselaraskan. Dengan semua kelas bersiri ini muncul, kemungkinan satu-satunya kelas yang perlu anda buat adalah siri anda sendiri, dan ini adalah sebilangan kue berbanding dengan menjalani setiap kelas dan menimpaclone() untuk membuat salinan mendalam.

Cara mudah untuk mengetahui jika anda mempunyai apa-apa kelas nonserializable dalam graf objek adalah untuk menganggap bahawa mereka semua serializable dan jangka ObjectCloner's deepCopy()kaedah di atasnya. Sekiranya terdapat objek yang kelasnya tidak dapat disenaraikan, maka java.io.NotSerializableExceptionwasiat akan dilemparkan, memberitahu anda kelas mana yang menyebabkan masalah.

Contoh pelaksanaan cepat ditunjukkan di bawah. Ia mewujudkan objek mudah, v1yang merupakan Vectoryang mengandungi Point. Objek ini kemudian dicetak untuk menunjukkan kandungannya. Objek asal, v1kemudian disalin ke objek baru vNew, yang dicetak untuk menunjukkan bahawa ia mengandungi nilai yang sama dengan v1. Seterusnya, isi v1diubah, dan akhirnya keduanya v1dan vNewdicetak sehingga nilainya dapat dibandingkan.

import java.util. *; import java.awt. *; public class Driver1 {static public void main (String [] args) {cuba {// dapatkan kaedah dari baris arahan String meth; if ((args.length == 1) && ((args [0] .equals ("deep")) || (args [0] .equals ("dangkal")))) {meth = args [0]; } lain {System.out.println ("Penggunaan: java Driver1 [dalam, cetek]"); kembali; } // buat objek asal Vektor v1 = Vektor baru (); Titik p1 = Titik baru (1,1); v1.addElement (p1); // lihat apa itu System.out.println ("Original =" + v1); Vektor vNew = null; if (met.equals ("deep")) {// salinan dalam vNew = (Vektor) (ObjectCloner.deepCopy (v1)); // A} lain jika (met.sama ("cetek")) {// salinan cetek vNew = (Vektor) v1.clone (); // B} // sahkan bahawa ia adalah System.out.println yang sama ("Baru =" + vNew);// ubah isi objek asal p1.x = 2; p1.y = 2; // lihat apa yang ada di dalamnya sekarang System.out.println ("Original =" + v1); System.out.println ("Baru =" + vBaru); } tangkapan (Pengecualian e) {System.out.println ("Pengecualian dalam utama =" + e); }}}

Untuk menggunakan salinan dalam (baris A), jalankan java.exe Driver1 deep. Apabila salinan dalam berjalan, kami mendapat cetakan berikut:

Asal = [java.awt.Point [x = 1, y = 1]] Baru = [java.awt.Point [x = 1, y = 1]] Asal = [java.awt.Point [x = 2, y = 2]] Baru = [java.awt.Point [x = 1, y = 1]] 

Ini menunjukkan bahawa apabila dokumen asal itu Point, p1, telah ditukar, baru Pointdicipta sebagai hasil daripada salinan yang mendalam tidak terjejas, kerana keseluruhan graf telah disalin. Sebagai perbandingan, gunakan salinan cetek (baris B) dengan melaksanakan java.exe Driver1 shallow. Apabila salinan cetek berjalan, kami mendapat cetakan berikut:

Asal = [java.awt.Point [x = 1, y = 1]] Baru = [java.awt.Point [x = 1, y = 1]] Asal = [java.awt.Point [x = 2, y = 2]] Baru = [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

Melaksanakan salinan mendalam grafik objek kompleks boleh menjadi tugas yang sukar. Teknik yang ditunjukkan di atas adalah alternatif mudah untuk prosedur konvensional menimpa clone()kaedah untuk setiap objek dalam grafik.

Dave Miller adalah arkitek kanan dengan firma perunding Javelin Technology, di mana dia bekerja pada aplikasi Java dan Internet. Dia telah bekerja di syarikat-syarikat seperti Hughes, IBM, Nortel, dan MCIWorldcom dalam projek berorientasikan objek, dan telah bekerja secara eksklusif dengan Java selama tiga tahun terakhir.

Ketahui lebih lanjut mengenai topik ini

  • Laman web Java Sun mempunyai bahagian yang didedikasikan untuk Spesifikasi Serialisasi Objek Java

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

Kisah ini, "Petua Java 76: Alternatif untuk teknik salin dalam" pada awalnya diterbitkan oleh JavaWorld.