Programovanie

Java Tip 76: Alternatíva k technike hlbokého kopírovania

Implementácia hlbokej kópie objektu môže byť zážitkom z učenia - dozviete sa, že to nechcete robiť! Ak predmetný objekt odkazuje na iné zložité objekty, ktoré zase odkazujú na iné, potom môže byť táto úloha skutočne skľučujúca. Každá trieda v objekte musí byť tradične individuálne skontrolovaná a upravená, aby sa mohla implementovať Cloneable rozhranie a prepísať jeho klon () metóda za účelom vytvorenia hĺbkovej kópie seba ako aj svojich obsiahnutých objektov. Tento článok popisuje jednoduchú techniku, ktorá sa dá použiť namiesto tejto časovo náročnej konvenčnej hĺbkovej kópie.

Koncept hlbokej kópie

Aby sme pochopili, čo a hlboká kópia je, najskôr sa pozrime na koncept plytkého kopírovania.

V predchádzajúcom JavaWorld článok „Ako sa vyhnúť nástrahám a správne prepísať metódy z java.lang.Object“, Mark Roulo vysvetľuje, ako klonovať objekty, aj ako dosiahnuť plytké kopírovanie namiesto hlbokého kopírovania. Ak to tu stručne zhrnieme, k plytkej kópii dôjde, keď sa objekt kopíruje bez jeho obsiahnutých objektov. Na ilustráciu, obrázok 1 zobrazuje objekt, obj1, ktorý obsahuje dva objekty, obsiahnutéObj1 a obsiahnutéObj2.

Ak sa vykonáva plytká kópia dňa obj1, potom sa skopíruje, ale obsiahnuté objekty nie sú, ako je znázornené na obrázku 2.

K hlbokej kópii dôjde, keď je objekt kopírovaný spolu s objektmi, na ktoré odkazuje. Obrázok 3 zobrazuje obj1 po vykonaní hĺbkovej kópie. Nielen, že má obj1 boli skopírované, ale boli tiež skopírované objekty v ňom obsiahnuté.

Ak niektorý z týchto obsiahnutých objektov sám obsahuje objekty, potom sa v hlbokej kópii skopírujú aj tieto objekty atď., Kým sa neprejde a neskopíruje celý graf. Každý objekt je zodpovedný za svoje klonovanie prostredníctvom svojich klon () metóda. Predvolená hodnota klon () metóda zdedená z Objekt, vytvorí plytkú kópiu objektu. Na dosiahnutie hlbokej kópie je potrebné pridať ďalšiu logiku, ktorá explicitne volá všetky obsiahnuté objekty ' klon () metódy, ktoré zase nazývajú svoje obsiahnuté objekty ' klon () metódy atď. Dosiahnutie tohto výsledku môže byť ťažké a časovo náročné a zriedka zábavné. Aby to bolo ešte komplikovanejšie, ak objekt nemožno priamo upravovať klon () metóda vytvorí plytkú kópiu, potom musí byť trieda rozšírená, klon () metóda prepísaná a táto nová trieda použitá namiesto starej. (Napríklad, Vektor neobsahuje logiku potrebnú pre hlbokú kópiu.) A ak chcete napísať kód, ktorý do runtime obracia otázku, či chcete vytvoriť hlbokú alebo plytkú kópiu objektu, ste v ešte zložitejšej situácii. V takom prípade musia existovať dve funkcie kopírovania pre každý objekt: jedna pre hlbokú kópiu a druhá pre plytké. Nakoniec, aj keď hlboko kopírovaný objekt obsahuje viac odkazov na iný objekt, druhý objekt by mal byť skopírovaný iba raz. Tým sa zabráni šíreniu objektov a skončí sa zvláštna situácia, keď kruhový odkaz vytvorí nekonečnú slučku kópií.

Serializácia

V januári 1998 JavaWorld inicioval jeho JavaBeans stĺpec Marka Johnsona s článkom o serializácii: „Robte to spôsobom„ Nescafé “- s lyofilizovanými JavaBeans.“ Stručne povedané, serializácia predstavuje schopnosť zmeniť graf objektov (vrátane zdegenerovaného prípadu jedného objektu) na pole bajtov, ktoré je možné zmeniť späť na ekvivalentný graf objektov. O objekte sa hovorí, že je serializovateľný, ak ho implementuje on alebo jeden z jeho predkov java.io. Serializovateľné alebo java.io.Externalizable. Serializovateľný objekt je možné serializovať jeho odovzdaním do domény writeObject () metóda ObjectOutputStream objekt. Týmto sa vypíšu primitívne dátové typy objektu, polia, reťazce a ďalšie odkazy na objekty. The writeObject () Potom sa na odkazované objekty zavolá metóda, ktorá ich tiež serializuje. Ďalej každý z týchto objektov má ich odkazy a objekty serializované; tento proces pokračuje ďalej a ďalej, až kým neprebehne celý graf a nebude serializovaný. Znie vám to povedome? Túto funkciu je možné použiť na dosiahnutie hlbokej kópie.

Hlboká kópia pomocou serializácie

Kroky na vytvorenie hlbokej kópie pomocou serializácie sú:

  1. Zaistite, aby všetky triedy v grafe objektu boli serializovateľné.

  2. Vytvorte vstupné a výstupné toky.

  3. Pomocou vstupných a výstupných prúdov môžete vytvoriť vstupné a výstupné prúdy objektov.

  4. Predajte objekt, ktorý chcete skopírovať, do výstupného prúdu objektu.

  5. Prečítajte si nový objekt zo vstupného toku objektov a odovzdajte ho späť do triedy odoslaného objektu.

Napísal som hodinu s názvom ObjectCloner ktorý implementuje kroky dva až päť. Riadok označený „A“ nastavuje a ByteArrayOutputStream ktorý sa používa na vytvorenie ObjectOutputStream na riadku B. Na čiare C sa čaruje. The writeObject () metóda rekurzívne prechádza grafom objektu, vygeneruje nový objekt v bajtovej podobe a odošle ho do ByteArrayOutputStream. Riadok D zabezpečuje, že bol odoslaný celý objekt. Kód na riadku E potom vytvorí a ByteArrayInputStream a naplní ho obsahom ByteArrayOutputStream. Riadok F vytvára inštancie ObjectInputStream pomocou ByteArrayInputStream vytvorený na riadku E a objekt je deserializovaný a vrátený do volacej metódy na riadku G. Tu je kód:

import java.io. *; import java.util. *; import java.awt. *; verejná trieda ObjectCloner {// tak, aby nikto nemohol náhodne vytvoriť objekt ObjectCloner, súkromný ObjectCloner () {} // vráti hlbokú kópiu objektu statický verejný objekt DeepCopy (Object oldObj) vyvolá výnimku {ObjectOutputStream oos = null; ObjectInputStream ois = null; try {ByteArrayOutputStream bos = nový ByteArrayOutputStream (); // A oos = new ObjectOutputStream (bos); // B // serializuj a odovzdaj objekt oos.writeObject (oldObj); // O o.flush (); // D ByteArrayInputStream bin = nový ByteArrayInputStream (bos.toByteArray ()); // E ois = new ObjectInputStream (bin); // F // vráti nový objekt return ois.readObject (); // G} catch (Výnimka e) {System.out.println ("Výnimka v ObjectCloner =" + e); hod (e); } nakoniec {oos.close (); ois.close (); }}} 

Všetci vývojári s prístupom k ObjectCloner pred spustením tohto kódu zostáva zabezpečiť, aby všetky triedy v grafe objektu boli serializovateľné. Vo väčšine prípadov to malo byť vykonané už; ak nie, malo by to byť s prístupom k zdrojovému kódu relatívne ľahké. Väčšina tried v JDK je serializovateľných; iba tie, ktoré sú závislé na platforme, ako napr FileDescriptor, niesu. Všetky triedy, ktoré získate od dodávateľa tretej strany a ktoré sú v súlade s JavaBean, sú tiež definitívne serializovateľné. Samozrejme, ak rozšírite triedu, ktorá je serializovateľná, nová trieda je tiež serializovateľná. Pretože všetky tieto serializovateľné triedy plávajú okolo, je pravdepodobné, že jediné, ktoré budete možno musieť serializovať, sú vaše vlastné, a to je v porovnaní s prechádzaním každou triedou a prepísaním hračkou. klon () urobiť hlbokú kópiu.

Jednoduchý spôsob, ako zistiť, či máte v grafe objektu nejaké neserializovateľné triedy, je predpokladať, že sú všetky serializovateľné a spustiteľné ObjectClonerje deepCopy () metóda na to. Ak existuje objekt, ktorého trieda nie je serializovateľná, potom a java.io.NotSerializableException bude vyhodený a povie vám, ktorá trieda spôsobila problém.

Nižšie je uvedený príklad rýchlej implementácie. Vytvára jednoduchý objekt, v1, čo je a Vektor ktorá obsahuje a Bod. Tento objekt sa potom vytlačí, aby sa zobrazil jeho obsah. Pôvodný objekt, v1, potom sa skopíruje do nového objektu, vNový, ktorý je vytlačený, aby ukázal, že obsahuje rovnakú hodnotu ako v1. Ďalej obsah v1 sú zmenené a nakoniec obidve v1 a vNový sú vytlačené, aby bolo možné ich hodnoty porovnať.

import java.util. *; import java.awt. *; public class Driver1 {static public void main (String [] args) {try {// získať metódu z príkazového riadku String pervitín; if ((args.length == 1) && ((args [0] .equals ("deep")) || (args [0] .equals ("shallow")))) {meth = args [0]; } else {System.out.println ("Použitie: java Driver1 [hlboký, plytký]"); návrat; } // vytvorenie pôvodného objektu Vector v1 = new Vector (); Bod p1 = nový bod (1,1); v1.addElement (p1); // pozri čo to je System.out.println ("Original =" + v1); Vektor vNew = null; if (meth.equals ("deep")) {// deep copy vNew = (Vector) (ObjectCloner.deepCopy (v1)); // A} else if (meth.equals ("plytký")) {// plytká kópia vNew = (Vector) v1.clone (); // B} // overiť, či ide o ten istý System.out.println ("Nový =" + vNew); // zmena obsahu pôvodného objektu p1.x = 2; pl.y = 2; // pozri, čo je teraz v každej z nich System.out.println ("Original =" + v1); System.out.println ("Nové =" + vNew); } catch (Výnimka e) {System.out.println ("Výnimka v main =" + e); }}} 

Ak chcete vyvolať hlbokú kópiu (riadok A), vykonajte príkaz java.exe Driver1 deep. Po spustení hlbokej kópie dostaneme nasledujúci výtlačok:

Originál = [java.awt.Point [x = 1, y = 1]] Nový = [java.awt.Point [x = 1, y = 1]] Originál = [java.awt.Point [x = 2, y = 2]] Nový = [java.awt.Point [x = 1, y = 1]] 

To ukazuje, že keď originál Bod, p1, bol zmenený, nový Bod vytvorené ako výsledok hĺbkovej kópie zostali nedotknuté, pretože bol skopírovaný celý graf. Pre porovnanie, vyvolajte plytkú kópiu (riadok B) vykonaním java.exe Driver1 plytký. Po spustení plytkej kópie dostaneme nasledujúci výtlačok:

Originál = [java.awt.Point [x = 1, y = 1]] Nový = [java.awt.Point [x = 1, y = 1]] Originál = [java.awt.Point [x = 2, y = 2]] Nový = [java.awt.Point [x = 2, y = 2]] 

To ukazuje, že keď originál Bod bol zmenený, nový Bod bol tiež zmenený. Je to spôsobené tým, že plytká kópia vytvára kópie iba odkazov, a nie objektov, na ktoré odkazujú. Toto je veľmi jednoduchý príklad, ale myslím si, že ilustruje bod, hm.

Problémy s implementáciou

Teraz, keď som kázal o všetkých vlastnostiach hlbokého kopírovania pomocou serializácie, pozrime sa na niektoré veci, na ktoré si treba dať pozor.

Prvým problematickým prípadom je trieda, ktorá nie je serializovateľná a ktorú nemožno upravovať. Môže sa to stať napríklad vtedy, ak používate triedu tretej strany, ktorá neobsahuje zdrojový kód. V takom prípade ho môžete rozšíriť, aby ste mohli implementovať rozšírenú triedu Serializovateľné, pridajte ľubovoľné (alebo všetky) potrebné konštruktory, ktoré práve volajú priradený nadkonštruktor, a použite túto novú triedu všade, kde ste robili starú (tu je príklad toho).

Môže sa to zdať ako veľa práce, ale iba ak v pôvodnej triede klon () metóda implementuje hlbokú kópiu, budete robiť niečo podobné za účelom jej prepísania klon () metóda tak ako tak.

Ďalším problémom je runtime rýchlosť tejto techniky. Ako si dokážete predstaviť, vytvorenie soketu, serializácia objektu, jeho prechod cez soket a následná deserializácia je pomalá v porovnaní s volaním metód v existujúcich objektoch. Tu je niekoľko zdrojových kódov, ktoré merajú čas potrebný na vykonanie obidvoch metód hĺbkového kopírovania (prostredníctvom serializácie a klon ()) na niektorých jednoduchých triedach a vytvára porovnávacie hodnoty pre rôzne počty iterácií. Výsledky zobrazené v milisekundách sú v nasledujúcej tabuľke:

Milisekundy na hlboké kopírovanie jednoduchého grafu triedy n-krát
Postup \ Iterácie (n)100010000100000
klon10101791
serializácia183211346107725

Ako vidíte, je veľký rozdiel vo výkone. Ak je kód, ktorý píšete, kritický pre výkon, možno budete musieť zahryznúť do guľky a ručne napísať hlbokú kópiu. Ak máte zložitý graf a máte jeden deň na to, aby ste implementovali hlbokú kópiu, a kód sa spustí ako dávková úloha v nedeľu ráno o jednej ráno, potom vám táto technika poskytne ďalšiu možnosť na zváženie.

Ďalším problémom je riešenie prípadu triedy, ktorej inštancie objektov vo virtuálnom stroji musia byť riadené. Toto je špeciálny prípad vzoru Singleton, v ktorom má trieda iba jeden objekt vo VM. Ako je uvedené vyššie, pri serializácii objektu vytvoríte úplne nový objekt, ktorý nebude jedinečný. Na obídenie tohto predvoleného správania môžete použiť readResolve () metóda na vynútenie toku, aby vrátil vhodný objekt, a nie ten, ktorý bol serializovaný. V tomto najmä V takom prípade je vhodný objekt ten istý, ktorý bol serializovaný. Tu je príklad toho, ako implementovať readResolve () metóda. Môžete sa dozvedieť viac o readResolve () ako aj ďalšie podrobnosti o serializácii na webovej stránke spoločnosti Sun venovanej špecifikácii serializácie objektov Java (pozri Zdroje).

Posledným, na čo si treba dať pozor, je prípad prechodných premenných. Ak je premenná označená ako prechodná, nebude sa serializovať, a preto sa nebude kopírovať ani ona, ani jej graf. Namiesto toho bude hodnotou prechodnej premennej v novom objekte predvolené hodnoty jazyka Java (null, false a nula). Nebudú sa vyskytovať žiadne chyby kompilácie ani doby behu, ktoré môžu mať za následok správanie, ktoré je ťažké ladiť. Samotné uvedomenie si toho môže ušetriť veľa času.

Technika hlbokého kopírovania môže programátorovi ušetriť mnoho hodín práce, ale môže spôsobiť vyššie popísané problémy. Predtým, ako sa rozhodnete, ktorú metódu použijete, nezabudnite ako vždy zvážiť výhody a nevýhody.

Záver

Implementácia hĺbkovej kópie grafu zložitého objektu môže byť náročná úloha. Vyššie uvedená technika je jednoduchou alternatívou k obvyklému postupu prepisu klon () metóda pre každý objekt v grafe.

Dave Miller je senior architekt v konzultačnej spoločnosti Javelin Technology, kde pracuje na Java a internetových aplikáciách. Pracoval pre spoločnosti ako Hughes, IBM, Nortel a MCIWorldcom na objektovo orientovaných projektoch a posledné tri roky pracoval výhradne s programom Java.

Získajte viac informácií o tejto téme

  • Webová stránka spoločnosti Sun spoločnosti Sun má časť venovanú špecifikácii serializácie objektov Java

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

Tento príbeh, „Java Tip 76: Alternatíva k technike hĺbkového kopírovania“, bol pôvodne publikovaný spoločnosťou JavaWorld.

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