Ohjelmointi

Java-vinkki 76: Vaihtoehto syväkopiointitekniikalle

Kohteen syvällisen kopion toteuttaminen voi olla oppimiskokemus - opit, ettet halua tehdä sitä! Jos kyseinen esine viittaa muihin monimutkaisiin kohteisiin, jotka puolestaan ​​viittaavat muihin, tämä tehtävä voi olla todella pelottava. Perinteisesti objektin jokainen luokka on tarkastettava ja muokattava erikseen Kloonattava käyttöliittymä ja ohittaa sen klooni() menetelmällä, jotta voidaan tehdä syvä kopio itsestään ja sen sisältämistä esineistä. Tässä artikkelissa kuvataan yksinkertainen tekniikka, jota voidaan käyttää tämän aikaa vievän perinteisen syväkopion tilalla.

Syvän kopion käsite

Ymmärtääkseen mitä a syvä kopio Tarkastellaan ensin matalan kopioinnin käsitettä.

Edellisessä JavaWorld artikkeli "Kuinka välttää ansoja ja ohittaa java.lang.Object-menetelmät oikein", Mark Roulo kertoo, kuinka objektit kloonataan ja miten saavutetaan matala kopiointi syvän kopioinnin sijaan. Lyhyesti sanottuna: matala kopio tapahtuu, kun objekti kopioidaan ilman sen sisältämiä esineitä. Havainnollistamiseksi kuvassa 1 on objekti, obj1, joka sisältää kaksi objektia, sisälsi Obj1 ja sisälsi Obj2.

Jos tehdään matala kopio obj1, sitten se kopioidaan, mutta sen sisältämät objektit eivät ole, kuten kuvassa 2 on esitetty.

Syvä kopio tapahtuu, kun objekti kopioidaan yhdessä kohteiden kanssa, joihin se viittaa. Kuva 3 esittää obj1 sen jälkeen, kun sille on tehty syvä kopio. Ei vain ole obj1 kopioitu, mutta myös sen sisältämät esineet on kopioitu.

Jos jompikumpi näistä sisältämistä objekteista itsessään sisältää esineitä, sitten syväkopiona kopioidaan myös nämä objektit ja niin edelleen, kunnes koko kaavio kulkee ja kopioidaan. Jokainen esine on vastuussa itsensä kloonaamisesta sen kautta klooni() menetelmä. Oletusarvo klooni() menetelmä, peritty Esine, tekee matalan kopion esineestä. Syväkopion saavuttamiseksi on lisättävä ylimääräinen logiikka, joka kutsuu kaikki sisältämät objektit nimenomaisesti klooni() menetelmiä, jotka puolestaan ​​kutsuvat niiden sisältämiä esineitä klooni() menetelmiä ja niin edelleen. Tämän oikeaksi saaminen voi olla vaikeaa ja aikaa vievää, ja on harvoin hauskaa. Tee asioista entistä monimutkaisempia, jos objektia ei voida muokata suoraan ja sen klooni() - menetelmä tuottaa matalan kopion, luokkaa on jatkettava, klooni() menetelmä ohitettiin, ja tätä uutta luokkaa käytettiin vanhan sijasta. (Esimerkiksi, Vektori ei sisällä syväkopion edellyttämää logiikkaa.) Ja jos haluat kirjoittaa koodin, joka lykkää ajonaikaan kysymystä siitä, tehdäänkö syvä vai matala kopio objektista, olet vielä monimutkaisemmassa tilanteessa. Tällöin kullekin objektille on oltava kaksi kopiointitoimintoa: yksi syvälle ja toinen matalalle. Lopuksi, vaikka syväkopioitava objekti sisältää useita viittauksia toiseen objektiin, jälkimmäinen objekti tulisi silti kopioida vain kerran. Tämä estää esineiden leviämisen ja johtaa erityistilanteeseen, jossa pyöreä viittaus tuottaa loputtoman kopiosilmukan.

Sarjaus

Takaisin tammikuussa 1998 JavaWorld aloitti sen Java-pavut Mark Johnson -sarakkeen sarjallisuutta käsittelevällä artikkelilla "Tee se Nescafé-tavalla - pakastekuivattuilla JavaBeans-kanavilla". Yhteenvetona voidaan todeta, että sarjallisuus on kyky muuttaa kuvaaja objekteista (mukaan lukien yksittäisen objektin rappeutunut tapaus) tavujoukoksi, joka voidaan muuttaa takaisin vastaavaksi objektien graafiksi. Esineen sanotaan olevan sarjoitettavissa, jos se tai joku sen esi-isistä toteuttaa java.io.Serialisoitavissa tai java.io. ulkoistettavissa. Sarjoitettava objekti voidaan sarjoittaa välittämällä se writeObject () menetelmä ObjectOutputStream esine. Tämä kirjoittaa objektin primitiiviset tietotyypit, taulukot, merkkijonot ja muut objektiviitteet. writeObject () menetelmää kutsutaan sitten viitatuille kohteille myös niiden sarjoimiseksi. Lisäksi jokaisella näistä esineistä on heidän viitteet ja esineet sarjoitettu; tämä prosessi jatkuu ja jatkuu, kunnes koko kaavio käydään läpi ja sarjoitetaan. Kuulostaako tämä tutulta? Tätä toimintoa voidaan käyttää syväkopiointiin.

Syvä kopiointi sarjallisuuden avulla

Vaiheet syväkopion tekemiseen sarjallisuuden avulla ovat:

  1. Varmista, että kaikki objektin kaavion luokat ovat sarjoitettavissa.

  2. Luo tulo- ja lähtövirrat.

  3. Käytä tulo- ja lähtövirtoja luodaksesi objektin syöttö- ja objektivirrat.

  4. Siirrä kopioitava objekti objektin ulostulovirtaan.

  5. Lue uusi objekti objektin syöttövirrasta ja heitä se takaisin lähettämäsi objektin luokkaan.

Olen kirjoittanut luokan nimeltä ObjectCloner joka toteuttaa vaiheet kaksi viiteen. "A" -merkillä varustettu viiva muodostaa a TavuArrayOutputStream jota käytetään luomaan ObjectOutputStream linjalla B. Taika tehdään linjalla C. writeObject () method kulkee rekursiivisesti objektin kaaviossa, luo uuden objektin tavumuodossa ja lähettää sen TavuArrayOutputStream. Rivi D varmistaa, että koko esine on lähetetty. Rivillä E oleva koodi luo sitten a TavuArrayInputStream ja täyttää sen TavuArrayOutputStream. Viiva F välittää välittömästi ObjectInputStream käyttämällä TavuArrayInputStream luodaan linjalla E ja objekti deserialisoidaan ja palautetaan linjan G. kutsumenetelmään. Tässä on koodi:

tuo java.io. *; tuo java.util. *; tuo java.awt. *; public class ObjectCloner {// jotta kukaan ei voi vahingossa luoda ObjectCloner-objektia private ObjectCloner () {} // palauttaa syvän kopion staattisesta julkisesta Object deepCopy (Object oldObj) heittää poikkeuksen {ObjectOutputStream oos = null; ObjectInputStream ois = tyhjä; kokeile {ByteArrayOutputStream bos = uusi ByteArrayOutputStream (); // A oos = new ObjectOutputStream (bos); // B // sarjoittaa ja välitä objekti oos.writeObject (oldObj); // C oos.flush (); // D ByteArrayInputStream bin = new ByteArrayInputStream (bos.toByteArray ()); // E ois = new ObjectInputStream (bin); // F // palauttaa uuden objektin return ois.readObject (); // G} catch (Exception e) {System.out.println ("Exception in ObjectCloner =" + e); heitto (e); } lopuksi {oos.close (); ois.close (); }}} 

Kaikki kehittäjät, joilla on käyttöoikeus ObjectCloner on tehtävä ennen tämän koodin suorittamista, on varmistettava, että kaikki objektin kaavion luokat ovat sarjoitettavissa. Useimmissa tapauksissa tämä olisi pitänyt tehdä jo; jos ei, sen pitäisi olla suhteellisen helppo tehdä pääsy lähdekoodiin. Suurin osa JDK: n luokista on sarjoitettavissa; vain ne, jotka ovat riippuvaisia ​​alustasta, kuten FileDescriptor, eivät ole. Lisäksi kaikki kolmannen osapuolen toimittajalta saamasi luokat, jotka ovat JavaBean-yhteensopivia, ovat määritelmän mukaan sarjoitettavissa. Tietysti, jos laajennat luokkaa, joka on sarjoitettavissa, uusi luokka on myös sarjoitettavissa. Kun kaikki nämä sarjoitettavat luokat kelluvat, on todennäköistä, että ainoat, jotka saatat tarvita sarjallisuuteen, ovat omia, ja tämä on pala kakkua verrattuna kunkin luokan läpikäyntiin ja korvaamiseen klooni() tehdä syvä kopio.

Helppo tapa selvittää, onko sinulla objektin kaaviossa ei-alustavia luokkia, on olettaa, että ne kaikki ovat sarjoitettavia ja suoritettavissa ObjectCloneron deepCopy () menetelmä. Jos on objektia, jonka luokkaa ei voi sarjoittaa, niin a java.io.NotSerializableException heitetään ja kerrotaan mikä luokka aiheutti ongelman.

Nopea toteutusesimerkki on esitetty alla. Se luo yksinkertaisen objektin, v1, joka on Vektori joka sisältää a Kohta. Tämä esine tulostetaan sitten sen sisällön näyttämiseksi. Alkuperäinen esine, v1, kopioidaan sitten uuteen objektiin, vUusi, joka on painettu osoittamaan, että se sisältää saman arvon kuin v1. Seuraavaksi sisältö v1 ovat muuttuneet, ja lopulta molemmat v1 ja vUusi tulostetaan, jotta niiden arvoja voidaan verrata.

tuo java.util. *; tuo java.awt. *; public class Driver1 {staattinen public void main (String [] args) {try {// hanki menetelmä komentoriviltä String meth; jos ((args.pituus == 1) && ((args [0]. on yhtä suuri ("syvä")) || (args [0]. yhtä suuri ("matala")))) {meth = args [0]; } else {System.out.println ("Käyttö: java-ohjain1 [syvä, matala]"); palata; } // luo alkuperäinen objekti Vektori v1 = uusi vektori (); Piste p1 = uusi piste (1,1); v1.addElement (p1); // katso mikä se on System.out.println ("Original =" + v1); Vektori vNew = nolla; if (met.ekvenssit ("syvä")) {// syvä kopio vNew = (vektori) (ObjectCloner.deepCopy (v1)); // A} else if (meth.equals ("matala")) {// matala kopio vNew = (Vector) v1.clone (); // B} // varmista, että se on sama System.out.println ("Uusi =" + vUusi); // muuta alkuperäisen objektin sisältöä p1.x = 2; p1.y = 2; // katso, mikä on jokaisessa nyt System.out.println ("Original =" + v1); System.out.println ("Uusi =" + vUusi); } catch (Poikkeus e) {System.out.println ("Exception in main =" + e); }}} 

Käynnistä syväkopio (rivi A) suorittamalla java.exe-ohjain1 syvä. Kun syväkopio suoritetaan, saamme seuraavan tulosteen:

Alkuperäinen = [java.awt.Point [x = 1, y = 1]] Uusi = [java.awt.Point [x = 1, y = 1]] Alkuperäinen = [java.awt.Point [x = 2, y = 2]] Uusi = [java.awt.Point [x = 1, y = 1]] 

Tämä osoittaa, että kun alkuperäinen Kohta, p1, muutettiin, uusi Kohta syväkopion tuloksena luotu ei vaikuttanut, koska koko kaavio kopioitiin. Vertailun vuoksi käytä matalaa kopiota (rivi B) suorittamalla java.exe Driver1 matala. Kun matala kopio suoritetaan, saamme seuraavan tulosteen:

Alkuperäinen = [java.awt.Point [x = 1, y = 1]] Uusi = [java.awt.Point [x = 1, y = 1]] Alkuperäinen = [java.awt.Point [x = 2, y = 2]] Uusi = [java.awt.Point [x = 2, y = 2]] 

Tämä osoittaa, että kun alkuperäinen Kohta muutettiin, uusi Kohta muutettiin myös. Tämä johtuu siitä, että matala kopio tekee kopioita vain viitteistä, ei esineistä, joihin ne viittaavat. Tämä on hyvin yksinkertainen esimerkki, mutta mielestäni se havainnollistaa ajatusta.

Toteutusongelmat

Nyt kun olen saarnannut kaikista syväkopioinnin hyveistä sarjallisuuden avulla, katsotaanpa joitain varovaisia ​​asioita.

Ensimmäinen ongelmallinen tapaus on luokka, jota ei voi sarjata ja jota ei voida muokata. Näin voi käydä esimerkiksi, jos käytät kolmannen osapuolen luokkaa, joka ei tule lähdekoodin mukana. Tässä tapauksessa voit laajentaa sitä, tehdä laajennetun luokan työkoneesta Sarjattavissa, lisää kaikki (tai kaikki) tarvittavat rakentajat, jotka vain kutsuvat liittyvää superrakentajaa, ja käytä tätä uutta luokkaa kaikkialla, missä teit vanhan (tässä on esimerkki tästä).

Tämä saattaa tuntua paljon työltä, mutta ellei alkuperäisen luokan ole klooni() menetelmä toteuttaa syväkopion, teet jotain vastaavaa ohittaaksesi sen klooni() menetelmä joka tapauksessa.

Seuraava numero on tämän tekniikan ajonopeus. Kuten voitte kuvitella, pistorasian luominen, objektin sarjallisuus, sen johtaminen pistorasian läpi ja sen poistaminen käytöstä on hidasta verrattuna olemassa olevien objektien soittomenetelmiin. Tässä on joitain lähdekoodeja, jotka mittaavat molempien syväkopiointimenetelmien suorittamiseen kuluvan ajan (sarjakuvan ja klooni()) joillakin yksinkertaisilla luokilla, ja tuottaa vertailuarvoja eri iterointimäärille. Millisekunteina esitetyt tulokset ovat alla olevassa taulukossa:

Millisekuntia yksinkertaisen luokan kuvaajan kopioimiseksi n kertaa
Menettely \ iteraatiot (n)100010000100000
klooni10101791
sarjallisuus183211346107725

Kuten näette, suorituskyvyssä on suuri ero. Jos kirjoittamasi koodi on suorituskyvyn kannalta kriittinen, joudut ehkä joutumaan puremaan luettelomerkin ja käsikoodaamaan syväkopion. Jos sinulla on monimutkainen kaavio ja sinulle annetaan yksi päivä syväkopion toteuttamiseen, ja koodi suoritetaan erätyönä sunnuntaisin aamulla, niin tämä tekniikka antaa sinulle uuden vaihtoehdon harkita.

Toinen asia koskee luokan tapausta, jonka objektien esiintymiä virtuaalikoneessa on hallittava. Tämä on Singleton-mallin erityistapaus, jossa luokassa on vain yksi objekti virtuaalikoneessa. Kuten yllä keskusteltiin, kun sarjoitat objektia, luot täysin uuden objektin, joka ei ole ainutlaatuinen. Voit kiertää tämän oletuskäyttäytymisen käyttämällä readResolve () menetelmä pakottaa virta palauttamaan sopiva objekti eikä sarjoitettu. Tässä tietty Tällöin sopiva objekti on sama, joka on sarjatuotettu. Tässä on esimerkki readResolve () menetelmä. Voit saada lisätietoja readResolve () sekä muut sarjoitustiedot Sunin verkkosivustolta, joka on omistettu Java Object Serialization Specificationille (katso Resurssit).

Viimeinen varovainen varoitus on transienttimuuttujien tapaus. Jos muuttuja on merkitty ohimeneväksi, sitä ei järjestetä, joten sitä ja sen kuvaajaa ei kopioida. Sen sijaan uuden objektin transienttimuuttujan arvo on Java-kielen oletusarvot (nolla, epätosi ja nolla). Ei tule käännös- tai ajonaikaisia ​​virheitä, jotka voivat johtaa vaikeasti virheenkorjattavaan toimintaan. Pelkästään tietoinen siitä voi säästää paljon aikaa.

Syväkopiointitekniikka voi säästää ohjelmoijaa monta tuntia työtä, mutta se voi aiheuttaa edellä kuvattuja ongelmia. Kuten aina, muista punnita edut ja haitat ennen kuin päätät käytettävästä menetelmästä.

Johtopäätös

Monimutkaisen objektikaavion syväkopion toteuttaminen voi olla vaikea tehtävä. Edellä esitetty tekniikka on yksinkertainen vaihtoehto tavanomaiselle menettelylle korvata klooni() menetelmä kaikille kaavion kohteille.

Dave Miller on vanhempi arkkitehti konsulttiyrityksessä Javelin Technology, jossa hän työskentelee Java- ja Internet-sovellusten parissa. Hän on työskennellyt esimerkiksi Hughesin, IBM: n, Nortelin ja MCIWorldcomin yrityksissä objektorisoiduissa projekteissa, ja viimeisten kolmen vuoden ajan hän on työskennellyt yksinomaan Javan kanssa.

Lisätietoja tästä aiheesta

  • Sunin Java-verkkosivustolla on osio, joka on omistettu Java Object Serialization Specificationille

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

Tämän tarinan "Java Tip 76: Alternative to deep copy tekniikka" julkaisi alun perin JavaWorld.

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