Programování

Java Tip 76: Alternativa k technice hlubokého kopírování

Implementace hluboké kopie objektu může být zážitkem z učení - dozvíte se, že to nechcete dělat! Pokud dotyčný objekt odkazuje na jiné složité objekty, které zase odkazují na jiné, pak může být tento úkol skutečně skličující. Tradičně musí být každá třída v objektu individuálně zkontrolována a upravena, aby bylo možné implementovat Cloneable rozhraní a přepsat jeho klon () metoda za účelem vytvoření hluboké kopie sebe sama i jejích obsažených objektů. Tento článek popisuje jednoduchou techniku, která se používá místo této časově náročné konvenční hluboké kopie.

Koncept hlubokých kopií

Abychom pochopili, co a hluboká kopie je, pojďme se nejprve podívat na koncept mělkého kopírování.

V předchozím JavaWorld článek „Jak se vyhnout pastím a správně přepsat metody z java.lang.Object,“ vysvětluje Mark Roulo, jak klonovat objekty a jak dosáhnout mělkého kopírování namísto hlubokého kopírování. Abychom to zde stručně shrnuli, k mělké kopii dochází, když je objekt kopírován bez jeho obsažených objektů. Pro ilustraci, obrázek 1 ukazuje objekt, obj1, který obsahuje dva objekty, containsObj1 a containsObj2.

Pokud se provádí mělká kopie obj1, pak je zkopírován, ale jeho obsažené objekty nejsou, jak je znázorněno na obrázku 2.

K hlubokému kopírování dochází, když je objekt kopírován spolu s objekty, na které odkazuje. Obrázek 3 ukazuje obj1 poté, co na něm byla provedena hluboká kopie. Nejen že má obj1 byly zkopírovány, ale byly zkopírovány také objekty v něm obsažené.

Pokud některý z těchto obsažených objektů sám obsahuje objekty, pak se v hloubkové kopii zkopírují také tyto objekty atd., Dokud nebude projet a zkopírován celý graf. Každý objekt je zodpovědný za klonování prostřednictvím svého klon () metoda. Výchozí klon () metoda zděděná z Objekt, vytvoří mělkou kopii objektu. K dosažení hluboké kopie je třeba přidat další logiku, která explicitně volá všechny obsažené objekty ' klon () metody, které zase volají jejich obsažené objekty ' klon () metody atd. Získání této správné informace může být obtížné a časově náročné a málokdy je zábavné. Aby to bylo ještě komplikovanější, pokud objekt nelze přímo upravit a jeho klon () metoda vytvoří mělkou kopii, potom musí být třída rozšířena, klon () metoda přepsána a tato nová třída použita místo staré. (Například, Vektor neobsahuje logiku nezbytnou pro hlubokou kopii.) A pokud chcete napsat kód, který do běhu odloží otázku, zda vytvořit hlubokou nebo mělkou kopii objektu, čeká vás ještě komplikovanější situace. V tomto případě musí existovat dvě funkce kopírování pro každý objekt: jedna pro hlubokou kopii a druhá pro mělkou. A konečně, i když hluboce kopírovaný objekt obsahuje více odkazů na jiný objekt, druhý objekt by měl být zkopírován pouze jednou. Tím se zabrání šíření objektů a dojde ke zvláštní situaci, kdy kruhový odkaz vytvoří nekonečnou smyčku kopií.

Serializace

V lednu 1998 JavaWorld zahájila svou JavaBeans sloupek od Marka Johnsona s článkem o serializaci „Udělejte to způsobem„ Nescafé “- s lyofilizovanými JavaBeans.“ Stručně řečeno, serializace je schopnost proměnit graf objektů (včetně zvráceného případu jediného objektu) na pole bajtů, které lze převést zpět na ekvivalentní graf objektů. O objektu se říká, že je serializovatelný, pokud jej nebo jeden z jeho předků implementuje java.io. Serializovatelné nebo java.io.Externalizable. Serializovatelný objekt lze serializovat předáním do writeObject () metoda ObjectOutputStream objekt. Tím se vypíšou primitivní datové typy objektu, pole, řetězce a další odkazy na objekty. The writeObject () Metoda je poté volána na odkazované objekty a také je serializovat. Dále každý z těchto objektů má jejich odkazy a objekty serializovány; tento proces pokračuje dál a dál, dokud není projet a serializován celý graf. Zní to povědomě? Tuto funkci lze použít k dosažení hluboké kopie.

Hluboká kopie pomocí serializace

Kroky pro vytvoření hluboké kopie pomocí serializace jsou:

  1. Zajistěte, aby všechny třídy v grafu objektu byly serializovatelné.

  2. Vytvářejte vstupní a výstupní toky.

  3. Pomocí vstupních a výstupních proudů můžete vytvářet vstupní a výstupní proudy objektů.

  4. Předejte objekt, který chcete zkopírovat do výstupního proudu objektu.

  5. Přečtěte si nový objekt ze vstupního proudu objektu a vraťte jej zpět do třídy odeslaného objektu.

Napsal jsem třídu s názvem ObjectCloner který implementuje kroky dva až pět. Řádek označený „A“ nastavuje a ByteArrayOutputStream který se používá k vytvoření ObjectOutputStream na řádku B. Na čáře C se kouzlo provádí. The writeObject () metoda rekurzivně prochází grafem objektu, generuje nový objekt v bajtové formě a odesílá jej do ByteArrayOutputStream. Řádek D zajišťuje odeslání celého objektu. Kód na řádku E poté vytvoří a ByteArrayInputStream a naplní jej obsahem ByteArrayOutputStream. Řádek F vytváří instanci ObjectInputStream za použití ByteArrayInputStream vytvořeno na řádku E a objekt je deserializován a vrácen do metody volání na řádku G. Zde je kód:

importovat java.io. *; import java.util. *; importovat java.awt. *; veřejná třída ObjectCloner {// aby nikdo nemohl náhodně vytvořit objekt ObjectCloner soukromý ObjectCloner () {} // vrátí hlubokou kopii objektu statický veřejný Objekt deepCopy (Object oldObj) vyvolá výjimku {ObjectOutputStream oos = null; ObjectInputStream ois = null; try {ByteArrayOutputStream bos = new ByteArrayOutputStream (); // A oos = new ObjectOutputStream (bos); // B // serializovat a předat objekt oos.writeObject (oldObj); // C oos.flush (); // D ByteArrayInputStream bin = nový ByteArrayInputStream (bos.toByteArray ()); // E ois = new ObjectInputStream (bin); // F // vrací nový objekt return ois.readObject (); // G} catch (Výjimka e) {System.out.println ("Výjimka v ObjectCloner =" + e); hod (e); } konečně {oos.close (); ois.close (); }}} 

Všichni vývojáři s přístupem k ObjectCloner před spuštěním tohoto kódu je třeba zajistit, aby všechny třídy v grafu objektu byly serializovatelné. Ve většině případů to mělo být provedeno již; pokud ne, mělo by to být relativně snadné s přístupem ke zdrojovému kódu. Většina tříd v JDK je serializovatelná; pouze ty, které jsou závislé na platformě, jako např FileDescriptor, nejsou. Také všechny třídy, které získáte od jiného dodavatele, které jsou kompatibilní s JavaBean, jsou podle definice serializovatelné. Samozřejmě, pokud rozšíříte třídu, která je serializovatelná, pak je nová třída také serializovatelná. Se všemi těmito serializovatelnými třídami, které se vznášejí, je pravděpodobné, že jediné, které budete možná potřebovat k serializaci, jsou vaše vlastní, a to je hračka ve srovnání s procházením každou třídou a přepisováním klon () udělat hlubokou kopii.

Snadný způsob, jak zjistit, zda máte v grafu objektu nějaké neserializovatelné třídy, je předpokládat, že jsou všechny serializovatelné a běží ObjectClonerje deepCopy () metoda na to. Pokud existuje objekt, jehož třída není serializovatelná, pak a java.io.NotSerializableException bude vyvolána a řekne vám, která třída způsobila problém.

Níže je uveden příklad rychlé implementace. Vytvoří jednoduchý objekt, v1, což je Vektor který obsahuje a Směřovat. Tento objekt se poté vytiskne, aby se zobrazil jeho obsah. Původní objekt, v1, je poté zkopírován do nového objektu, vNový, který je vytištěn, aby ukázal, že obsahuje stejnou hodnotu jako v1. Dále obsah v1 jsou změněny a nakonec oba v1 a vNový jsou vytištěny, aby bylo možné porovnat jejich hodnoty.

import java.util. *; importovat java.awt. *; public class Driver1 {static public void main (String [] args) {try {// získat metodu z příkazového řádku String meth; if ((args.length == 1) && ((args [0] .equals ("deep")) || (args [0] .equals ("shallow")))) {meth = args [0]; } else {System.out.println ("Použití: java Driver1 [hluboký, mělký]"); vrátit se; } // vytvoření původního objektu Vector v1 = new Vector (); Bod p1 = nový bod (1,1); v1.addElement (p1); // podívejte se, co 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 ("shallow")) {// mělká kopie vNew = (Vector) v1.clone (); // B} // ověřit, že jde o stejný System.out.println ("New =" + vNew); // změna obsahu původního objektu p1.x = 2; p1.y = 2; // podívejte se, co je v každém nyní System.out.println ("Original =" + v1); System.out.println ("Nový =" + vNew); } catch (Výjimka e) {System.out.println ("Výjimka v main =" + e); }}} 

Chcete-li vyvolat hlubokou kopii (řádek A), proveďte java.exe Driver1 deep. Když se spustí hluboká kopie, získáme následující výtisk:

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 když originál Směřovat, p1, byl změněn, nový Směřovat vytvořený v důsledku hluboké kopie zůstal nedotčen, protože byl zkopírován celý graf. Pro srovnání vyvolajte mělkou kopii (řádek B) provedením java.exe Driver1 mělký. Po spuštění mělké kopie získáme následující výtisk:

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 když originál Směřovat byl změněn, nový Směřovat bylo také změněno. To je způsobeno skutečností, že mělká kopie vytváří kopie pouze odkazů, a nikoli objektů, na které odkazují. Toto je velmi jednoduchý příklad, ale myslím, že ilustruje bod, hm.

Problémy s implementací

Nyní, když jsem kázal o všech výhodách hlubokého kopírování pomocí serializace, pojďme se podívat na některé věci, na které si dávat pozor.

Prvním problematickým případem je třída, která není serializovatelná a kterou nelze upravovat. To se může stát, například pokud používáte třídu třetí strany, která nepřichází se zdrojovým kódem. V tomto případě jej můžete rozšířit, provést implementaci rozšířené třídy Serializovatelné, přidejte všechny (nebo všechny) nezbytné konstruktory, které právě volají přidružený superkonstruktor, a použijte tuto novou třídu všude, kde jste provedli starou (zde je příklad).

To se může zdát jako hodně práce, ale pokud to není původní třída klon () metoda implementuje hlubokou kopii, budete dělat něco podobného, ​​abyste ji přepsali klon () metoda stejně.

Dalším problémem je rychlost běhu této techniky. Jak si dokážete představit, vytváření soketu, serializace objektu, jeho předávání soketem a jeho deserializace je pomalá ve srovnání s voláním metod v existujících objektech. Zde je nějaký zdrojový kód, který měří čas potřebný k provedení obou metod hlubokého kopírování (prostřednictvím serializace a klon ()) na některých jednoduchých třídách a vytváří měřítka pro různé počty iterací. Výsledky zobrazené v milisekundách jsou v následující tabulce:

Milisekundy k hlubokému kopírování jednoduchého grafu třídy nkrát
Postup \ Iterace (n)100010000100000
klon10101791
serializace183211346107725

Jak vidíte, ve výkonu je velký rozdíl. Pokud kód, který píšete, je kritický pro výkon, možná budete muset kousnout kulku a ručně kódovat hlubokou kopii. Pokud máte složitý graf a máte jeden den k implementaci hluboké kopie a kód bude spuštěn jako dávková úloha v jednu ráno v neděli, pak vám tato technika nabízí další možnost, kterou je třeba zvážit.

Dalším problémem je řešení případu třídy, jejíž instance objektů ve virtuálním stroji musí být řízeny. Toto je speciální případ vzoru Singleton, ve kterém má třída pouze jeden objekt v rámci virtuálního počítače. Jak je uvedeno výše, při serializaci objektu vytvoříte zcela nový objekt, který nebude jedinečný. Chcete-li obejít toto výchozí chování, můžete použít readResolve () metoda k vynucení proudu, aby vrátil vhodný objekt, spíše než ten, který byl serializován. V tomhle konkrétní v případě, že příslušný objekt je stejný, který byl serializován. Zde je příklad toho, jak implementovat readResolve () metoda. Můžete se dozvědět více o readResolve () stejně jako další podrobnosti o serializaci na webu společnosti Sun věnovaném specifikaci serializace objektů Java (viz Zdroje).

Poslední věcí, na kterou je třeba dávat pozor, je případ přechodných proměnných. Pokud je proměnná označena jako přechodná, nebude serializována, a proto nebude kopírována ani s grafem. Místo toho bude hodnotou přechodné proměnné v novém objektu výchozí nastavení jazyka Java (null, false a nula). Nebudou žádné chyby kompilace nebo běhu, což může mít za následek chování, které je těžké ladit. Pouhé vědomí toho může ušetřit spoustu času.

Technika hlubokého kopírování může programátorovi ušetřit mnoho hodin práce, ale může způsobit výše popsané problémy. Než se rozhodnete, kterou metodu použít, nezapomeňte zvážit výhody a nevýhody.

Závěr

Implementace hluboké kopie komplexního grafu objektu může být obtížný úkol. Výše uvedená technika je jednoduchou alternativou ke konvenčnímu postupu přepsání klon () metoda pro každý objekt v grafu.

Dave Miller je senior architekt v poradenské společnosti Javelin Technology, kde pracuje na aplikacích Java a Internet. Pracoval pro společnosti jako Hughes, IBM, Nortel a MCIWorldcom na objektově orientovaných projektech a poslední tři roky pracoval výhradně s Javou.

Další informace o tomto tématu

  • Web společnosti Sun na webu Java má část věnovanou specifikaci serializace objektů Java

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

Tento příběh, „Java Tip 76: Alternativa k technice hlubokého kopírování“, původně publikoval JavaWorld.