Tip Java 76: Alternatif untuk teknik deep copy

Menerapkan salinan mendalam dari suatu objek bisa menjadi pengalaman belajar - Anda belajar bahwa Anda tidak ingin melakukannya! Jika objek yang dimaksud mengacu pada objek kompleks lainnya, yang pada gilirannya merujuk ke objek lain, maka tugas ini memang bisa menjadi tugas yang menakutkan. Secara tradisional, setiap kelas dalam objek harus diperiksa dan diedit secara individual untuk mengimplementasikan Cloneableantarmuka dan mengganti clone()metodenya untuk membuat salinan mendalam dari dirinya sendiri serta objek yang ada di dalamnya. Artikel ini menjelaskan teknik sederhana untuk digunakan sebagai pengganti salinan dalam konvensional yang memakan waktu ini.

Konsep salinan dalam

Untuk memahami apa itu deep copy , pertama mari kita lihat konsep penyalinan dangkal.

Dalam artikel JavaWorld sebelumnya , "Cara menghindari jebakan dan mengganti metode dari java.lang.Object dengan benar," Mark Roulo menjelaskan cara mengkloning objek serta cara melakukan penyalinan dangkal, bukan penyalinan mendalam. Untuk meringkas secara singkat di sini, salinan dangkal terjadi ketika sebuah objek disalin tanpa objek di dalamnya. Untuk mengilustrasikan, Gambar 1 menunjukkan sebuah objek obj1,, yang berisi dua objek, containedObj1dan containedObj2.

Jika salinan dangkal dilakukan pada obj1, maka itu disalin tetapi objek yang ada di dalamnya tidak, seperti yang ditunjukkan pada Gambar 2.

Salinan dalam terjadi ketika sebuah objek disalin bersama dengan objek yang dirujuknya. Gambar 3 menunjukkan obj1setelah deep copy dilakukan di atasnya. Tidak hanya telah obj1disalin, tetapi objek yang ada di dalamnya telah disalin juga.

Jika salah satu dari objek yang dimuat ini sendiri berisi objek, maka, dalam salinan dalam, objek tersebut juga akan disalin, dan seterusnya hingga seluruh grafik dilintasi dan disalin. Setiap objek bertanggung jawab untuk mengkloning dirinya sendiri melalui clone()metodenya. clone()Metode default , diwarisi dari Object, membuat salinan objek yang dangkal. Untuk mendapatkan salinan dalam, logika tambahan harus ditambahkan yang secara eksplisit memanggil semua clone()metode objek yang ditampung , yang pada gilirannya memanggil clone()metode objek tertampungnya, dan seterusnya. Melakukannya dengan benar bisa jadi sulit dan memakan waktu, dan jarang menyenangkan. Untuk membuat hal-hal menjadi lebih rumit, jika sebuah objek tidak dapat dimodifikasi secara langsung dan clone()metodenya menghasilkan salinan yang dangkal, maka kelas harus diperpanjang,clone()metode diganti, dan kelas baru ini digunakan sebagai pengganti yang lama. (Misalnya, Vectortidak berisi logika yang diperlukan untuk deep copy.) Dan jika Anda ingin menulis kode yang menangguhkan hingga waktu proses pertanyaan apakah akan membuat salinan yang dalam atau dangkal suatu objek, Anda berada dalam situasi yang lebih rumit. situasi. Dalam hal ini, harus ada dua fungsi salinan untuk setiap objek: satu untuk salinan dalam dan satu lagi untuk dangkal. Terakhir, meskipun objek yang sedang disalin berisi banyak referensi ke objek lain, objek terakhir hanya boleh disalin sekali. Hal ini mencegah proliferasi objek, dan menghindari situasi khusus di mana referensi melingkar menghasilkan salinan berulang yang tak terbatas.

Serialisasi

Kembali pada bulan Januari 1998, JavaWorld memulai kolom JavaBeans oleh Mark Johnson dengan artikel tentang serialisasi, "Lakukan dengan cara 'Nescafé' - dengan JavaBeans yang dikeringkan." Untuk meringkas, 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. Sebuah objek dikatakan dapat berseri jika itu atau salah satu leluhurnya mengimplementasikan java.io.Serializableatau java.io.Externalizable. Objek yang dapat diserialisasi dapat diserialkan dengan meneruskannya ke writeObject()metode ObjectOutputStreamobjek. Ini menulis tipe data primitif objek, array, string, dan referensi objek lainnya. ItuwriteObject()metode kemudian dipanggil pada objek yang dirujuk untuk menserialisasinya juga. Selanjutnya, masing-masing objek memiliki mereka referensi dan benda-benda serial; proses ini terus berlanjut hingga seluruh grafik dilintasi dan diserialkan. Apakah ini terdengar familiar? Fungsionalitas ini dapat digunakan untuk mendapatkan salinan yang dalam.

Salinan mendalam menggunakan serialisasi

Langkah-langkah membuat deep copy menggunakan serialisasi adalah:

  1. Pastikan semua kelas dalam grafik objek dapat diserialkan.

  2. Buat aliran input dan output.

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

  4. Teruskan objek yang ingin Anda salin ke aliran output objek.

  5. Baca objek baru dari aliran input objek dan transmisikan kembali ke kelas objek yang Anda kirim.

Saya telah menulis sebuah kelas yang disebut ObjectCloneryang mengimplementasikan langkah dua hingga lima. Garis bertanda "A" set up a ByteArrayOutputStreamyang digunakan untuk membuat ObjectOutputStreamon line B. Line C adalah tempat keajaiban dilakukan. The writeObject()Metode rekursif melintasi grafik objek, menghasilkan objek baru dalam bentuk byte, dan mengirimkannya ke ByteArrayOutputStream. Baris D memastikan seluruh objek telah dikirim. Kode pada baris E kemudian membuat ByteArrayInputStreamdan mengisinya dengan konten file ByteArrayOutputStream. Baris F membuat instance ObjectInputStreammenggunakan yang ByteArrayInputStreamdibuat di baris E dan objek tersebut deserialisasi dan dikembalikan ke metode pemanggilan di baris G. Berikut kodenya:

impor java.io. *; import java.util. *; import java.awt. *; public class ObjectCloner {// sehingga tidak ada yang bisa secara tidak sengaja membuat objek ObjectCloner private ObjectCloner () {} // mengembalikan salinan dalam dari objek publik statis Object deepCopy (Object oldObj) melempar Exception {ObjectOutputStream oos = null; ObjectInputStream ois = null; coba {ByteArrayOutputStream bos = new ByteArrayOutputStream (); // A oos = new ObjectOutputStream (bos); // B // membuat serial dan meneruskan objek oos.writeObject (oldObj); // C oos.flush (); // D ByteArrayInputStream bin = ByteArrayInputStream baru (bos.toByteArray ()); // E ois = new ObjectInputStream (bin); // F // kembalikan objek baru return ois.readObject (); // G} catch (Exception e) {System.out.println ("Exception in ObjectCloner =" + e); lempar (e); } akhirnya {oos.close (); ois.close (); }}}

Semua pengembang dengan akses ObjectCloneryang harus dilakukan sebelum menjalankan kode ini memastikan bahwa semua kelas dalam grafik objek dapat diserialkan. Dalam kebanyakan kasus, ini seharusnya sudah dilakukan; jika tidak, seharusnya relatif mudah dilakukan dengan akses ke kode sumber. Sebagian besar kelas di JDK dapat diserialkan; hanya yang bergantung pada platform, seperti FileDescriptor, yang tidak. Selain itu, setiap kelas yang Anda dapatkan dari vendor pihak ketiga yang sesuai dengan JavaBean menurut definisi dapat diserialkan. Tentu saja, jika Anda memperluas kelas yang dapat diserialkan, maka kelas baru juga dapat dibuat bersambung. Dengan semua kelas yang dapat diserialkan ini beredar, kemungkinan satu-satunya yang Anda butuhkan untuk membuat serial adalah milik Anda sendiri, dan ini adalah sepotong kue dibandingkan dengan melewati setiap kelas dan menimpaclone() untuk melakukan salinan dalam.

Cara mudah untuk mengetahui apakah Anda memiliki kelas nonserializable dalam grafik obyek adalah dengan mengasumsikan bahwa mereka semua serializable dan menjalankan ObjectCloner's deepCopy()metode di atasnya. Jika ada objek yang kelasnya tidak dapat diserialisasi, maka a java.io.NotSerializableExceptionakan dilemparkan, memberi tahu Anda kelas mana yang menyebabkan masalah.

Contoh implementasi cepat ditunjukkan di bawah ini. Ini membuat objek sederhana v1, yang Vectorberisi file Point. Objek ini kemudian dicetak untuk menunjukkan isinya. Objek asli,, v1kemudian disalin ke objek baru vNew, yang dicetak untuk memperlihatkan bahwa itu berisi nilai yang sama dengan v1. Berikutnya, isi v1berubah, dan akhirnya kedua v1dan vNewdicetak sehingga nilai-nilai mereka dapat dibandingkan.

import java.util. *; import java.awt. *; public class Driver1 {static public void main (String [] args) {coba {// dapatkan metode dari baris perintah String meth; if ((args.length == 1) && ((args [0] .equals ("deep")) || (args [0] .equals ("shallow")))) {meth = args [0]; } else {System.out.println ("Penggunaan: java Driver1 [dalam, dangkal]"); kembali; } // buat objek asli Vector v1 = new Vector (); Titik p1 = Titik baru (1,1); v1.addElement (p1); // lihat apa itu System.out.println ("Original =" + v1); Vektor vNew = null; if (meth.equals ("deep")) {// deep copy vNew = (Vector) (ObjectCloner.deepCopy (v1)); // A} else if (meth.equals ("shallow")) {// shallow copy vNew = (Vector) v1.clone (); // B} // verifikasikan bahwa ini adalah System.out.println yang sama ("New =" + vNew);// ubah konten objek asli p1.x = 2; p1.y = 2; // lihat apa yang ada di masing-masing sekarang System.out.println ("Original =" + v1); System.out.println ("Baru =" + vNew); } catch (Exception e) {System.out.println ("Exception in main =" + e); }}}

Untuk memanggil salinan dalam (baris A), jalankan java.exe Driver1 deep. Saat deep copy dijalankan, kami mendapatkan hasil cetak berikut:

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

Ini menunjukkan bahwa ketika aslinya Point,, p1diubah, yang baru Pointdibuat sebagai hasil dari salinan dalam tetap tidak terpengaruh, karena seluruh grafik disalin. Sebagai perbandingan, panggil salinan dangkal (baris B) dengan menjalankan java.exe Driver1 shallow. Saat salinan dangkal berjalan, kami mendapatkan hasil cetak berikut:

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

Menerapkan salinan mendalam dari grafik objek yang kompleks bisa menjadi tugas yang sulit. Teknik yang ditunjukkan di atas adalah alternatif sederhana untuk prosedur konvensional dalam menimpa clone()metode untuk setiap objek dalam grafik.

Dave Miller adalah arsitek senior di perusahaan konsultan Javelin Technology, tempat dia bekerja pada aplikasi Java dan Internet. Dia telah bekerja untuk perusahaan seperti Hughes, IBM, Nortel, dan MCIWorldcom pada proyek berorientasi objek, dan telah bekerja secara eksklusif dengan Java selama tiga tahun terakhir.

Pelajari lebih lanjut tentang topik ini

  • Situs Web Sun Java memiliki bagian yang didedikasikan untuk Spesifikasi Serialisasi Objek Java

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

Artikel ini, "Java Tip 76: Sebuah alternatif untuk teknik deep copy" pada awalnya diterbitkan oleh JavaWorld.