Java Savjet 76: Alternativa tehnici dubokog kopiranja

Implementacija dubinske kopije predmeta može biti iskustvo učenja - naučite da to ne želite učiniti! Ako se predmetni objekt odnosi na druge složene objekte, koji se pak odnose na druge, tada ovaj zadatak može zaista biti zastrašujući. Tradicionalno, svaka klasa u objektu mora se pojedinačno pregledavati i uređivati ​​kako bi se implementiralo Cloneablesučelje i nadjačala njegova clone()metoda kako bi se napravila duboka kopija same sebe kao i sadržanih objekata. Ovaj članak opisuje jednostavnu tehniku ​​koja se koristi umjesto ove dugotrajne konvencionalne duboke kopije.

Pojam duboke kopije

Da bismo razumjeli što je duboka kopija , pogledajmo prvo pojam plitkog kopiranja.

U prethodnom članku JavaWorlda , "Kako izbjeći zamke i pravilno nadjačati metode iz java.lang.Object", Mark Roulo objašnjava kako klonirati objekte, kao i kako postići plitko kopiranje umjesto dubinskog kopiranja. Da ukratko rezimiramo ovdje, plitka kopija nastaje kada se objekt kopira bez sadržanih objekata. Za ilustraciju, slika 1 prikazuje objekt obj1, koji sadrži dva predmeta, containedObj1i containedObj2.

Ako se izvede plitka kopija obj1, ona se kopira, ali sadržani objekti to nisu, kao što je prikazano na slici 2.

Dubinska kopija događa se kada se objekt kopira zajedno s objektima na koje se odnosi. Slika 3 prikazuje obj1nakon što je na njemu napravljena dubinska kopija. Ne samo da je obj1kopirano, nego su kopirani i sadržani sadržaji u njemu.

Ako bilo koji od ovih sadržanih objekata sam sadrži objekte, tada se u dubinskoj kopiji kopiraju i ti objekti, i tako sve dok se cijeli grafikon ne pređe i ne kopira. Svaki je objekt odgovoran za kloniranje putem svoje clone()metode. Zadana clone()metoda, naslijeđena od Object, izrađuje plitku kopiju objekta. Da bi se postigla dubinska kopija, mora se dodati dodatna logika koja eksplicitno poziva clone()metode svih sadržanih objekata , koje zauzvrat nazivaju clone()metode svojih sadržanih objekata , i tako dalje. Ispraviti ovo može biti teško i dugotrajno, a rijetko je zabavno. Da stvar bude još složenija, ako se objekt ne može izravno modificirati i njegova clone()metoda stvori plitku kopiju, tada se klasa mora proširiti,clone()metoda nadjačana, a ova je nova klasa korištena umjesto stare. (Na primjer, Vectorne sadrži logiku potrebnu za dubinsku kopiju.) A ako želite napisati kod koji odgađa do vremena izvođenja pitanje treba li napraviti duboku ili plitku kopiju objekta, pred vama je još složenija situacija. U ovom slučaju moraju postojati dvije funkcije kopiranja za svaki objekt: jedna za dubinsku kopiju i jedna za plitku. Napokon, čak i ako objekt koji se duboko kopira sadrži više referenci na drugi objekt, potonji bi objekt trebao biti kopiran samo jednom. To sprječava širenje predmeta i uklanja posebnu situaciju u kojoj kružna referenca stvara beskonačnu petlju kopija.

Serijalizacija

Još u siječnju 1998. godine JavaWorld pokrenuo je svoju kolumnu JavaBeans Marka Johnsona člankom o serializaciji "Učini to na način" Nescafé "- zamrznuto osušenim JavaBeansom." Da rezimiramo, serializacija je sposobnost pretvaranja grafa objekata (uključujući degenerirani slučaj jednog predmeta) u niz bajtova koji se mogu pretvoriti natrag u ekvivalentan graf objekata. Kaže se da je objekt moguće serializirati ako on ili jedan od njegovih predaka primijeni java.io.Serializableili java.io.Externalizable. Objekt koji se može serirati može se serializirati prenošenjem na writeObject()metodu ObjectOutputStreamobjekta. Ovo ispisuje primitivne tipove podataka objekta, nizove, nizove i druge reference na objekt. ThewriteObject()metoda se zatim poziva na predmetne objekte kako bi ih i serializirala. Nadalje, svaki od ovih objekata ima svoje reference i serializirane predmete; ovaj postupak traje sve dok se cijeli grafikon ne pređe i serializira. Zvuči li ovo poznato? Ova se funkcija može koristiti za dubinsko kopiranje.

Dubinska kopija pomoću serializacije

Koraci za izradu dubinske kopije pomoću serializacije su:

  1. Osigurajte da se sve klase u grafu objekta mogu serializirati.

  2. Stvorite ulazne i izlazne tokove.

  3. Upotrijebite ulazni i izlazni tok za stvaranje ulaznih i izlaznih tokova objekta.

  4. Predaj objekt koji želiš kopirati u izlazni tok objekta.

  5. Pročitajte novi objekt iz ulaznog toka objekta i vratite ga u klasu objekta koji ste poslali.

Napisao sam klasu ObjectClonerkoja provodi korake od dva do pet. Linija s oznakom "A" postavlja znak ByteArrayOutputStreamkoji se koristi za stvaranje ObjectOutputStreamon-line B. Linija C je mjesto na kojem se magija vrši. writeObject()Metoda rekurzivno prolazi objekta graf, stvara novi objekt u byte obliku, i šalje ga na ByteArrayOutputStream. Red D osigurava da je poslan cijeli objekt. Kôd na retku E tada kreira a ByteArrayInputStreami popunjava ga sadržajem ByteArrayOutputStream. Red F instancira ObjectInputStreamupotrebu ByteArrayInputStreamkreiranog na liniji E, a objekt se deserijalizira i vraća na metodu pozivanja na liniji G. Evo koda:

import java.io. *; uvoz java.util. *; import java.awt. *; javna klasa ObjectCloner {// tako da nitko ne može slučajno stvoriti ObjectCloner objekt private ObjectCloner () {} // vraća dubinsku kopiju objekta statički javni objekt Object deepCopy (Object oldObj) baca izuzetak {ObjectOutputStream oos = null; ObjectInputStream ois = null; isprobajte {ByteArrayOutputStream bos = new ByteArrayOutputStream (); // A oos = novi ObjectOutputStream (bos); // B // serializirati i proslijediti objekt oos.writeObject (oldObj); // C oos.flush (); // D ByteArrayInputStream bin = novi ByteArrayInputStream (bos.toByteArray ()); // E ois = novi ObjectInputStream (bin); // F // vrati novi objekt return ois.readObject (); // G} catch (Iznimka e) {System.out.println ("Iznimka u ObjectCloner =" + e); baciti (e); } napokon {oos.close (); ois.close (); }}}

Sve što je potrebno učiniti programeru s pristupom ObjectClonerprije izvođenja ovog koda jest osigurati da se sve klase u grafikonu objekta mogu serializirati. U većini slučajeva to je već trebalo biti učinjeno; ako ne, to bi trebalo biti relativno lako učiniti s pristupom izvornom kodu. Većina klasa u JDK mogu se serirati; samo oni koji ovise o platformi, kao što FileDescriptorsu, nisu. Također, sve klase koje dobijete od nezavisnog dobavljača, a koje su u skladu s JavaBean-om, po definiciji se mogu serializirati. Naravno, ako proširite klasu koja se može serializirati, tada se i nova klasa može serializirati. Sa svim ovim serijskim klasama koje lebde, velika je vjerojatnost da su jedini koji će vam trebati serializirati vaši vlastiti, a ovo je komad kolača u usporedbi s prolaskom kroz svaki razred i prepisivanjemclone() napraviti duboku kopiju.

Jednostavan način da saznate ako imate bilo kakvih nonserializable klase u objektno-a graf je pretpostaviti da su svi serializable i trčanje ObjectClonerje deepCopy()metoda na njega. Ako postoji objekt čija se klasa ne može serializirati, tada java.io.NotSerializableExceptionće se baciti a, govoreći vam koja je klasa uzrokovala problem.

Primjer brze implementacije prikazan je u nastavku. To stvara jednostavan objekt, v1koji je Vectorto sadrži Point. Zatim se ovaj objekt ispisuje kako bi prikazao njegov sadržaj. Izvorni objekt v1,, zatim se kopira u novi objekt vNewkoji se ispisuje kako bi se pokazalo da sadrži istu vrijednost kao v1. Dalje se mijenja sadržaj v1i na kraju oboje v1i vNewispisuju se kako bi se mogle usporediti njihove vrijednosti.

uvoz java.util. *; import java.awt. *; javna klasa Driver1 {static public void main (String [] args) {try {// preuzmi metodu iz naredbenog retka String meth; if ((args.length == 1) && ((args [0] .equals ("deep")) || (args [0] .equals ("shallow")))) {meth = args [0]; } else {System.out.println ("Upotreba: java Driver1 [duboko, plitko]"); povratak; } // stvoriti izvorni objekt Vector v1 = new Vector (); Točka p1 = nova Točka (1,1); v1.addElement (p1); // pogledajte što je System.out.println ("Original =" + v1); Vektor vNew = null; if (meth.equals ("deep")) {// dubinska kopija vNew = (Vector) (ObjectCloner.deepCopy (v1)); // A} else if (meth.equals ("plitko")) {// plitka kopija vNew = (Vector) v1.clone (); // B} // provjeri radi li se o istom System.out.println ("Novo =" + vNew);// promjena sadržaja izvornog objekta p1.x = 2; p1.y = 2; // pogledajte što se sada nalazi u svakom od njih System.out.println ("Original =" + v1); System.out.println ("Novo =" + vNew); } catch (Iznimka e) {System.out.println ("Iznimka u main =" + e); }}}

Da biste pozvali dubinsku kopiju (redak A), izvršite java.exe Driver1 deep. Kada se dubinska kopija pokrene, dobit ćemo sljedeći ispis:

Izvornik = [java.awt.Point [x = 1, y = 1]] Novo = [java.awt.Point [x = 1, y = 1]] Izvornik = [java.awt.Point [x = 2, y = 2]] Novo = [java.awt.Point [x = 1, y = 1]] 

To pokazuje da kad je izvorni Point, p1, promijenjen, novi Pointnastala kao rezultat dubokog primjerak je ostao nepromijenjen, jer je cijeli graf je kopiran. Za usporedbu, izvršenjem pozovite plitku kopiju (redak B) java.exe Driver1 shallow. Kad se plitka kopija pokrene, dobit ćemo sljedeći ispis:

Izvornik = [java.awt.Point [x = 1, y = 1]] Novo = [java.awt.Point [x = 1, y = 1]] Izvornik = [java.awt.Point [x = 2, y = 2]] Novo = [java.awt.Point [x = 2, y = 2]] 

To pokazuje da kada Pointse promijenio izvornik , promijenio se i novi Point. To je zbog činjenice da plitka kopija pravi kopije samo referenci, a ne i predmeta na koje se odnose. Ovo je vrlo jednostavan primjer, ali mislim da ilustrira, um, poantu.

Pitanja provedbe

Sad kad sam propovijedao o svim vrlinama dubokog kopiranja pomoću serializacije, pogledajmo neke stvari na koje treba pripaziti.

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

Implementacija dubinske kopije složenog grafa objekta može biti težak zadatak. Gore prikazana tehnika jednostavna je alternativa uobičajenom postupku prepisivanja clone()metode za svaki objekt na grafikonu.

Dave Miller stariji je arhitekt u konzultantskoj tvrtki Javelin Technology, gdje radi na Javi i internetskim aplikacijama. Radio je za tvrtke poput Hughesa, IBM-a, Nortela i MCIWorldcom na objektno orijentiranim projektima, a posljednje tri godine radio je isključivo s Javom.

Saznajte više o ovoj temi

  • Sunino web mjesto Java ima odjeljak posvećen Specifikaciji serizacije Java objekata

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

Ovu priču, "Java Savjet 76: Alternativa tehnici dubokog kopiranja" izvorno je objavio JavaWorld.