Programiranje

Java Nasvet 76: Alternativa tehniki globokega kopiranja

Izvajanje globoke kopije predmeta je lahko učna izkušnja - naučite se, da tega ne želite storiti! Če se zadevni objekt nanaša na druge zapletene predmete, ti pa na druge, potem je ta naloga lahko zastrašujoča. Tradicionalno je treba vsak razred v objektu pregledati in urediti, da se izvede Klonirano vmesnik in ga preglasite klon () metoda, da naredi globoko kopijo samega sebe in njegovih predmetov. Ta članek opisuje preprosto tehniko, ki jo je treba uporabiti namesto te zamudne običajne globoke kopije.

Koncept globoke kopije

Da bi razumeli, kaj a globoka kopija je, poglejmo najprej koncept plitkega kopiranja.

V prejšnjem JavaWorld članek, "Kako se izogniti pasti in pravilno preglasiti metode iz java.lang.Object," Mark Roulo razloži, kako klonirati predmete in kako doseči plitvo kopiranje namesto globokega kopiranja. Če na kratko povzamem tukaj, pride do plitve kopije, ko se objekt kopira brez predmetov v njem. Za ponazoritev slika 1 prikazuje predmet, obj1, ki vsebuje dva predmeta, vsebujeObj1 in vsebujeObj2.

Če se plitka kopija izvede obj1, nato se kopira, vendar njegovi predmeti v njej niso, kot je prikazano na sliki 2.

Globoka kopija se pojavi, ko se objekt kopira skupaj s predmeti, na katere se nanaša. Slika 3 prikazuje obj1 potem ko je bila na njem narejena globoka kopija. Ne samo, da je obj1 kopirali, kopirali pa so se tudi predmeti v njem.

Če kateri koli od teh vsebin vsebuje predmete, se v globoki kopiji kopirajo tudi ti predmeti in tako naprej, dokler ne preide in kopira celotnega grafa. Vsak predmet je sam odgovoren za kloniranje prek svojega klon () metoda. Privzeto klon () metoda, podedovana od Predmet, naredi plitvo kopijo predmeta. Za globoko kopijo je treba dodati dodatno logiko, ki izrecno prikliče vse vsebovane predmete ' klon () metode, ki nato pokličejo svoje vsebovane predmete ' klon () metode itd. To je lahko težavno in dolgotrajno ter redko zabavno. Da bodo stvari še bolj zapletene, če predmeta ni mogoče neposredno spremeniti in njegovega klon () metoda ustvari plitvo kopijo, potem je treba razred razširiti, klon () metodo preglasil in ta novi razred uporabil namesto starega. (Na primer, Vektor ne vsebuje logike, ki je potrebna za globoko kopijo.) In če želite napisati kodo, ki do izvedbe odloži vprašanje, ali narediti globoko ali plitvo kopijo predmeta, vas čaka še bolj zapletena situacija. V tem primeru morata biti za vsak objekt dve funkciji kopiranja: ena za globoko kopijo in ena za plitvo. Nazadnje, tudi če objekt, ki se globoko kopira, vsebuje več sklicev na drug objekt, je treba slednji objekt še vedno kopirati le enkrat. To preprečuje širjenje predmetov in odpravlja posebne razmere, v katerih krožna referenca ustvari neskončno zanko kopij.

Serializacija

Januarja 1998 JavaWorld začela svojo JavaBeans stolpec Marka Johnsona s člankom o serializaciji, "Naredi to na način Nescafé - z liofiliziranim JavaBeans." Če povzamemo, serializacija je zmožnost pretvorbe grafa predmetov (vključno z izrojenim primerom posameznega predmeta) v niz bajtov, ki jih je mogoče spremeniti nazaj v enakovreden graf predmetov. Predmet naj bi bil serializirati, če ga izvaja eden od njegovih prednikov java.io.Serializable ali java.io.Externalizable. Predmet, ki ga je mogoče serializirati, lahko serializiramo tako, da ga posredujemo v writeObject () metoda an ObjectOutputStream predmet. S tem se izpišejo primitivni podatkovni tipi predmeta, nizi, nizi in druge reference na objekt. The writeObject () Nato se za napotene predmete pokliče metoda, da jih tudi serializira. Poleg tega ima vsak od teh predmetov njihovi reference in predmeti, serializirani; ta postopek traja in traja, dokler se celoten graf ne prevozi in serializira. Se to sliši znano? To funkcijo lahko uporabite za globinsko kopiranje.

Globoka kopija z uporabo serializacije

Koraki za izdelavo globoke kopije z uporabo serializacije so:

  1. Prepričajte se, da je mogoče vse razrede v grafu predmeta serializirati.

  2. Ustvarite vhodne in izhodne tokove.

  3. Z vhodnimi in izhodnimi tokovi ustvarite vhodne in izhodne tokove predmetov.

  4. Predmet, ki ga želite kopirati, posredujte v izhodni tok predmeta.

  5. Preberite nov predmet iz vhodnega toka predmeta in ga vrnite v razred predmeta, ki ste ga poslali.

Sem napisal predavanje z imenom ObjectCloner ki izvaja korake od drugega do petega. Vrstica z oznako "A" postavlja a ByteArrayOutputStream ki se uporablja za ustvarjanje ObjectOutputStream na liniji B. V liniji C je čarovnija. The writeObject () metoda rekurzivno prečka graf predmeta, ustvari nov objekt v bajtni obliki in ga pošlje v ByteArrayOutputStream. Vrstica D zagotavlja, da je bil celoten predmet poslan. Koda v vrstici E nato ustvari a ByteArrayInputStream in ga zapolni z vsebino ByteArrayOutputStream. Vrstica F ustvari ObjectInputStream uporabljati ByteArrayInputStream ustvarjen v vrstici E in objekt je deserializiran in vrnjen klicni metodi v vrstici G. Tu je koda:

uvoz java.io. *; uvoz java.util. *; uvoz java.awt. *; javni razred ObjectCloner {// tako, da nihče ne more slučajno ustvariti predmeta ObjectCloner private ObjectCloner () {} // vrne globoko kopijo predmeta statični javni objekt Object deepCopy (Object oldObj) vrže izjemo {ObjectOutputStream oos = null; ObjectInputStream ois = null; poskusite {ByteArrayOutputStream bos = new ByteArrayOutputStream (); // A oos = nov ObjectOutputStream (bos); // B // serializiramo in posredujemo objekt oos.writeObject (oldObj); // C oos.flush (); // D ByteArrayInputStream bin = nov ByteArrayInputStream (bos.toByteArray ()); // E ois = nov ObjectInputStream (bin); // F // vrne nov objekt return ois.readObject (); // G} ulov (izjema e) {System.out.println ("Izjema v ObjectCloner =" + e); met (e); } končno {oos.close (); ois.close (); }}} 

Vsi razvijalci z dostopom do ObjectCloner pred zagonom te kode je treba zagotoviti, da so vsi razredi v grafu predmeta serializacijski. V večini primerov bi to morali že storiti; če ne, bi moralo biti razmeroma enostavno opraviti z dostopom do izvorne kode. Večino razredov v JDK je mogoče serializirati; samo tiste, ki so odvisne od platforme, kot npr FileDescriptor, niso. Vsi razredi, ki jih dobite od neodvisnega prodajalca in so skladni z JavaBean, so po definiciji serializacijski. Seveda, če razširite razred, ki ga je mogoče serializirati, potem je tudi nov razred serializirati. Z vsemi temi serializacijskimi razredi, ki plujejo okoli, obstaja verjetnost, da so edini, ki jih boste morda potrebovali za serializacijo, vaši in to je kos pogače v primerjavi z ogledom vsakega razreda in prepisovanjem klon () narediti globoko kopijo.

Enostaven način, da ugotovite, ali imate v grafu predmeta kakršne koli neserializacijske razrede, je predpostaviti, da so vsi serializirani in se izvajajo ObjectClonerje deepCopy () metoda na njem. Če obstaja objekt, katerega razreda ni mogoče serializirati, potem a java.io.NotSerializableException vrgel vas bo in vam povedal, kateri razred je povzročil težavo.

Primer hitre izvedbe je prikazan spodaj. Ustvari preprost predmet, v1, ki je a Vektor ki vsebuje a Točka. Ta predmet se nato natisne, da se prikaže njegova vsebina. Prvotni predmet, v1, se nato kopira v nov objekt, vNovo, ki je natisnjen, da pokaže, da vsebuje enako vrednost kot v1. Nato vsebina v1 se spremenijo in na koncu oba v1 in vNovo so natisnjene, da lahko primerjamo njihove vrednosti.

uvoz java.util. *; uvoz java.awt. *; javni razred Driver1 {static public void main (String [] args) {try {// pridobi metodo iz ukazne vrstice String meth; if ((args.length == 1) && ((args [0] .equals ("deep")) || (args [0] .equals ("plitve")))) {meth = args [0]; } else {System.out.println ("Uporaba: java Driver1 [globoko, plitvo]"); vrnitev; } // ustvarimo izvirni objekt Vector v1 = new Vector (); Točka p1 = nova točka (1,1); v1.addElement (p1); // poglejte, kaj je System.out.println ("Original =" + v1); Vektor vNew = null; if (meth.equals ("deep")) {// globoka kopija vNew = (Vector) (ObjectCloner.deepCopy (v1)); // A} else if (meth.equals ("plitvo")) {// plitka kopija vNew = (Vector) v1.clone (); // B} // preverimo, da gre za isti System.out.println ("Novo =" + vNew); // spremenimo vsebino izvirnega predmeta p1.x = 2; p1.y = 2; // oglejte si, kaj je zdaj v vsakem od njih System.out.println ("Original =" + v1); System.out.println ("Novo =" + vNew); } catch (Izjema e) {System.out.println ("Izjema v main =" + e); }}} 

Če želite priklicati globoko kopijo (vrstica A), izvedite java.exe Gonilnik1 globoko. Ko se poglobljena kopija zažene, dobimo naslednji izpis:

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

To kaže, da ko original Točka, p1, je bil spremenjen, nov Točka ustvarjena kot rezultat globoke kopije, je ostala nespremenjena, saj je bil kopiran celoten graf. Za primerjavo pokličite plitvo kopijo (vrstica B) z izvajanjem java.exe Gonilnik1 plitev. Ko se plitva kopija zažene, dobimo naslednji izpis:

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

To kaže, da ko original Točka je bila spremenjena, nova Točka je bil tudi spremenjen. To je posledica dejstva, da plitva kopija naredi samo sklice, ne pa tudi predmetov, na katere se nanašajo. To je zelo preprost primer, vendar mislim, da ponazarja to, hm.

Vprašanja pri izvajanju

Zdaj, ko sem pridigal o vseh vrlinah globokega kopiranja s pomočjo serializacije, poglejmo nekaj stvari, na katere moramo biti pozorni.

Prvi problematični primer je razred, ki ga ni mogoče serializirati in ga ni mogoče urejati. To se lahko zgodi, na primer, če uporabljate tuji razred, ki nima izvorne kode. V tem primeru ga lahko razširite, razširjeni razred izvedite Serializabilno, dodajte katere koli (ali vse) potrebne konstruktorje, ki samo pokličejo pripadajoči superkonstruktor, in uporabite ta novi razred povsod, kjer ste storili starega (tukaj je primer tega).

To se morda zdi veliko dela, vendar razen v primeru prvotnega predavanja klon () metoda izvaja globoko kopijo, naredili boste nekaj podobnega, da bi to preglasili klon () metoda vseeno.

Naslednje vprašanje je hitrost izvajanja te tehnike. Kot si lahko predstavljate, je ustvarjanje vtičnice, serializacija predmeta, njegovo podajanje skozi vtičnico in nato deserializacija počasna v primerjavi s klicanjem metod v obstoječih predmetih. Tu je nekaj izvorne kode, ki meri čas, potreben za izvedbo obeh metod globokega kopiranja (s serializacijo in klon ()) v nekaterih preprostih razredih in ustvarja merila uspešnosti za različno število ponovitev. Rezultati, prikazani v milisekundah, so v spodnji tabeli:

Milisekunde za globoko kopiranje preprostega grafa razreda n-krat
Postopek \ Ponavljanja (n)100010000100000
klon10101791
serializacija183211346107725

Kot lahko vidite, obstaja velika razlika v zmogljivosti. Če je koda, ki jo pišete, kritična za delovanje, boste morda morali ugrizniti kroglo in ročno kodirati globoko kopijo. Če imate zapleten graf in imate en dan za izvedbo globoke kopije in se bo koda izvajala kot paketno opravilo ob eni uri zjutraj ob nedeljah, potem vam ta tehnika ponuja še eno možnost.

Druga težava je obravnava primera razreda, katerega primere v virtualnem stroju je treba nadzorovati. To je poseben primer vzorca Singleton, v katerem ima razred samo en objekt znotraj VM. Kot smo že omenili, ko serializirate predmet, ustvarite popolnoma nov objekt, ki ne bo unikaten. Če želite zaobiti to privzeto vedenje, lahko uporabite readResolve () metoda, da prisili tok, da vrne ustrezen objekt namesto tistega, ki je bil serializiran. V tem posebno primer, je ustrezen objekt isti, ki je bil serializiran. Tu je primer, kako uporabiti readResolve () metoda. Več o tem lahko izveste readResolve () kot tudi druge podrobnosti o serializaciji na spletnem mestu Sun, posvečenem specifikaciji Java Object Serialization Specification (glejte Viri).

Še zadnje, na kar moramo biti pozorni, je primer prehodnih spremenljivk. Če je spremenljivka označena kot prehodna, potem ne bo serializirana in zato ne bo kopirana. Namesto tega bo vrednost prehodne spremenljivke v novem objektu privzete vrednosti jezika Java (null, false in zero). Ne bo nobenih napak pri prevajanju ali izvajanju, kar lahko povzroči vedenje, ki ga je težko odpraviti. Že samo zavedanje tega lahko prihrani veliko časa.

Tehnika globokega kopiranja lahko programerju prihrani veliko ur dela, lahko pa povzroči zgoraj opisane težave. Kot vedno se prepričajte, da ste pretehtali prednosti in slabosti, preden se odločite, katero metodo boste uporabili.

Zaključek

Izvajanje globoke kopije zapletenega grafa predmeta je lahko težka naloga. Zgoraj prikazana tehnika je preprosta alternativa običajnemu postopku prepisa klon () metoda za vsak objekt v grafu.

Dave Miller je višji arhitekt v svetovalnem podjetju Javelin Technology, kjer dela na Javi in ​​internetnih aplikacijah. Pri objektno usmerjenih projektih je delal za podjetja, kot so Hughes, IBM, Nortel in MCIWorldcom, zadnja tri leta pa je sodeloval izključno z Javo.

Preberite več o tej temi

  • Sunino spletno mesto Java vsebuje odsek, namenjen specifikaciji serizacije predmetov Java

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

To zgodbo, "Java Nasvet 76: Alternativa tehniki globokega kopiranja", je prvotno objavil JavaWorld.

$config[zx-auto] not found$config[zx-overlay] not found