Programmering

Java Tip 76: Et alternativ til teknikken til dyb kopiering

Implementering af en dyb kopi af et objekt kan være en læringsoplevelse - du lærer, at du ikke vil gøre det! Hvis det pågældende objekt refererer til andre komplekse objekter, som igen henviser til andre, kan denne opgave faktisk være skræmmende. Traditionelt skal hver klasse i objektet inspiceres individuelt og redigeres for at implementere Klonabel interface og tilsidesætte dens klon () metode for at lave en dyb kopi af sig selv såvel som dens indeholdte objekter. Denne artikel beskriver en simpel teknik til brug i stedet for denne tidskrævende konventionelle dybe kopi.

Begrebet dyb kopi

For at forstå, hvad en dyb kopi er, lad os først se på begrebet lav kopiering.

I en tidligere JavaWorld artikel, "Sådan undgår du fælder og tilsidesætter korrekt metoder fra java.lang.Object", forklarer Mark Roulo, hvordan man kloner objekter såvel som hvordan man opnår lav kopiering i stedet for dyb kopiering. For at opsummere kort her opstår der en lav kopi, når et objekt kopieres uden dets indeholdte objekter. For at illustrere viser figur 1 et objekt, obj1, der indeholder to objekter, indeholdtObj1 og indeholdtObj2.

Hvis en lav kopi udføres den obj1, så kopieres den, men dens indeholdte objekter er ikke, som vist i figur 2.

En dyb kopi opstår, når et objekt kopieres sammen med de objekter, det refererer til. Figur 3 viser obj1 efter at der er udført en dyb kopi på den. Ikke kun har obj1 kopieret, men genstandene indeholdt i den er også kopieret.

Hvis en af ​​disse indeholdte objekter selv indeholder objekter, kopieres disse objekter også i en dyb kopi og så videre, indtil hele grafen krydses og kopieres. Hvert objekt er ansvarlig for at klone sig selv via dets klon () metode. Standardindstillingen klon () metode, arvet fra Objekt, laver en lav kopi af objektet. For at opnå en dyb kopi skal der tilføjes ekstra logik, der eksplicit kalder alle indeholdte objekter ' klon () metoder, som igen kalder deres indeholdte objekter ' klon () metoder og så videre. At få dette korrekt kan være svært og tidskrævende og sjældent sjovt. For at gøre tingene endnu mere komplicerede, hvis et objekt ikke kan ændres direkte og dets klon () metoden producerer en lav kopi, så skal klassen udvides, klon () metode tilsidesat, og denne nye klasse bruges i stedet for den gamle. (For eksempel, Vektor indeholder ikke den logik, der er nødvendig for en dyb kopi.) Og hvis du vil skrive kode, der afviger indtil runtime, spørgsmålet om, hvorvidt du skal lave en dyb eller lav kopi til et objekt, er du i en endnu mere kompliceret situation. I dette tilfælde skal der være to kopifunktioner for hvert objekt: en til en dyb kopi og en til en lavvandet. Endelig, selvom det objekt, der kopieres dybt, indeholder flere referencer til et andet objekt, skal sidstnævnte objekt stadig kun kopieres en gang. Dette forhindrer spredning af objekter og afværger den særlige situation, hvor en cirkulær reference frembringer en uendelig løkke af kopier.

Serialisering

Tilbage i januar 1998, JavaWorld indledte sin JavaBeans kolonne af Mark Johnson med en artikel om serialisering, "Gør det på 'Nescafé' måde - med frysetørrede JavaBeans." For at opsummere er serialisering evnen til at omdanne en graf med objekter (inklusive det degenererede tilfælde af et enkelt objekt) til en række bytes, der kan omdannes til en ækvivalent graf med objekter. Et objekt siges at kunne serienummeres, hvis det eller en af ​​dets forfædre implementerer java.io.Serialiserbar eller java.io. kan udvides. Et seriøst objekt kan serieiseres ved at sende det til writeObject () metode til en ObjectOutputStream objekt. Dette skriver objektets primitive datatyper, arrays, strenge og andre objektreferencer ud. Det writeObject () metoden kaldes derefter på de henviste objekter for også at serieisere dem. Yderligere har hver af disse objekter deres referencer og genstande serielt; denne proces fortsætter og fortsætter, indtil hele grafen krydses og serialiseres. Lyder det velkendt? Denne funktionalitet kan bruges til at opnå en dyb kopi.

Dyb kopi ved hjælp af serialisering

Trin til at lave en dyb kopi ved hjælp af serialisering er:

  1. Sørg for, at alle klasser i objektets graf kan serienummeres.

  2. Opret input- og output-streams.

  3. Brug input- og outputstrømmene til at oprette objektinput- og objektoutputstrømme.

  4. Send det objekt, du vil kopiere, til objektets outputstrøm.

  5. Læs det nye objekt fra objektets inputstrøm, og kast det tilbage til klassen for det objekt, du sendte.

Jeg har skrevet en klasse kaldet ObjectCloner der implementerer trin to til fem. Linjen mærket "A" opretter en ByteArrayOutputStream som bruges til at oprette ObjectOutputStream på linje B. Linie C er hvor magien udføres. Det writeObject () metode krydser rekursivt objektets graf, genererer et nyt objekt i byteform og sender det til ByteArrayOutputStream. Linje D sikrer, at hele objektet er sendt. Koden på linje E opretter derefter en ByteArrayInputStream og udfylder det med indholdet af ByteArrayOutputStream. Linje F instantierer en ObjectInputStream bruger ByteArrayInputStream oprettet på linje E, og objektet deserialiseres og returneres til kaldemetoden på linje G. Her er koden:

import java.io. *; importer java.util. *; import java.awt. *; offentlig klasse ObjectCloner {// så ingen ved et uheld kan oprette et ObjectCloner-objekt privat ObjectCloner () {} // returnerer en dyb kopi af et objekt statisk offentlig Object deepCopy (Object oldObj) kaster Undtagelse {ObjectOutputStream oos = null; ObjectInputStream ois = null; prøv {ByteArrayOutputStream bos = ny ByteArrayOutputStream (); // A oos = ny ObjectOutputStream (bos); // B // serialisere og videregive objektet oos.writeObject (oldObj); // C oos.flush (); // D ByteArrayInputStream bin = ny ByteArrayInputStream (bos.toByteArray ()); // E ois = ny ObjectInputStream (bin); // F // returnere det nye objekt returnere ois.readObject (); // G} catch (Undtagelse e) {System.out.println ("Undtagelse i ObjectCloner =" + e); kast (e); } endelig {oos.close (); ois.close (); }}} 

Alt sammen en udvikler med adgang til ObjectCloner er tilbage at gøre, før denne kode køres, er at sikre, at alle klasser i objektets graf kan serienummeres. I de fleste tilfælde burde dette allerede være gjort; hvis ikke, burde det være relativt let at gøre med adgang til kildekoden. De fleste af klasser i JDK kan serienummeres; kun dem, der er platformafhængige, f.eks FileDescriptor, er ikke. Desuden kan alle klasser, du får fra en tredjepartsleverandør, der er JavaBean-kompatible, pr. Definition serierbare. Selvfølgelig, hvis du udvider en klasse, der kan serialiseres, så kan den nye klasse også serialiseres. Med alle disse serierbare klasser, der flyder rundt, er chancerne for, at de eneste, du muligvis har brug for at serialisere, er dine egne, og dette er et stykke kage i forhold til at gå igennem hver klasse og overskrive klon () at lave en dyb kopi.

En nem måde at finde ud af, om du har klasser, der ikke kan omseries i et objekts graf, er at antage, at de alle kan serienummeres og køres ObjectCloner's deepCopy () metode på det. Hvis der er et objekt, hvis klasse ikke kan serienummeres, så er a java.io.NotSerializableException vil blive kastet og fortælle dig, hvilken klasse der forårsagede problemet.

Et eksempel på hurtig implementering er vist nedenfor. Det skaber et simpelt objekt, v1, som er en Vektor der indeholder en Punkt. Dette objekt udskrives derefter for at vise dets indhold. Den originale genstand, v1, kopieres derefter til et nyt objekt, vNyt, der udskrives for at vise, at den indeholder den samme værdi som v1. Dernæst indholdet af v1 ændres, og til sidst begge dele v1 og vNyt udskrives, så deres værdier kan sammenlignes.

importer java.util. *; import java.awt. *; public class Driver1 {static public void main (String [] args) {try {// få metoden fra kommandolinjen String meth; if ((args.length == 1) && ((args [0] .equals ("deep")) || (args [0] .equals ("shallow")))) {meth = args [0]; } ellers {System.out.println ("Brug: java Driver1 [dyb, lavvandet]"); Vend tilbage; } // opret originalt objekt Vector v1 = ny Vector (); Punkt p1 = nyt punkt (1,1); v1.addElement (p1); // se hvad det er System.out.println ("Original =" + v1); Vector vNew = null; if (meth.equals ("deep")) {// deep copy vNew = (Vector) (ObjectCloner.deepCopy (v1)); // A} andet hvis (meth.equals ("lavvandet")) {// lav kopi vNew = (Vector) v1.clone (); // B} // bekræft, at det er det samme System.out.println ("Ny =" + vNy); // ændre det originale objekts indhold p1.x = 2; p1.y = 2; // se hvad der er i hver nu System.out.println ("Original =" + v1); System.out.println ("Ny =" + vNy); } fange (Undtagelse e) {System.out.println ("Undtagelse i main =" + e); }}} 

For at påkalde den dybe kopi (linje A) skal du udføre java.exe Driver1 dyb. Når den dybe kopi kører, får vi følgende udskrift:

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

Dette viser, at når originalen Punkt, p1, blev ændret, den nye Punkt oprettet som et resultat af den dybe kopi forblev upåvirket, da hele grafen blev kopieret. Til sammenligning påberåbes den lave kopi (linje B) ved at udføre java.exe Driver1 lavt. Når den lave kopi kører, får vi følgende udskrift:

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

Dette viser, at når originalen Punkt blev ændret, den nye Punkt blev også ændret. Dette skyldes det faktum, at den lave kopi kun kopierer referencerne og ikke de objekter, de henviser til. Dette er et meget simpelt eksempel, men jeg synes, det illustrerer punktet um.

Implementeringsspørgsmål

Nu hvor jeg har prædiket om alle dyderne ved dyb kopiering ved hjælp af serialisering, lad os se på nogle ting at passe på.

Det første problematiske tilfælde er en klasse, der ikke kan serienummeres, og som ikke kan redigeres. Dette kan f.eks. Ske, hvis du bruger en tredjepartsklasse, der ikke følger med kildekoden. I dette tilfælde kan du udvide det, gøre det udvidede klasseværktøj Serialiserbar, tilføj eventuelle (eller alle) nødvendige konstruktører, der bare kalder den tilknyttede superkonstruktør, og brug denne nye klasse overalt, hvor du gjorde den gamle (her er et eksempel på dette).

Dette kan virke som en masse arbejde, men medmindre den oprindelige klasse er klon () metode implementerer dyb kopi, vil du gøre noget lignende for at tilsidesætte dets klon () metode alligevel.

Det næste spørgsmål er kørehastigheden for denne teknik. Som du kan forestille dig, er det langsomt at oprette en stikkontakt, serieisere et objekt, føre det gennem stikkontakten og deserialisere det igen i forhold til kaldemetoder i eksisterende objekter. Her er nogle kildekoder, der måler den tid, det tager at lave begge dybe kopimetoder (via serialisering og klon ()) på nogle enkle klasser og producerer benchmarks for forskellige antal iterationer. Resultaterne, vist i millisekunder, er i nedenstående tabel:

Millisekunder til dybkopiering af en simpel klassediagram n gange
Procedure \ Iterationer (n)100010000100000
klon10101791
serialisering183211346107725

Som du kan se, er der en stor forskel i ydeevne. Hvis koden, du skriver, er ydeevnekritisk, kan det være nødvendigt at bide kuglen og håndkode en dyb kopi. Hvis du har en kompleks graf og får en dag til at implementere en dyb kopi, og koden køres som et batchjob en om morgenen om søndagen, så giver denne teknik dig en anden mulighed at overveje.

Et andet problem handler om sagen om en klasse, hvis objekters forekomster inden for en virtuel maskine skal styres. Dette er et specielt tilfælde af Singleton-mønsteret, hvor en klasse kun har et objekt inden for en VM. Som diskuteret ovenfor, når du serieliserer et objekt, opretter du et helt nyt objekt, der ikke vil være unikt. For at omgå denne standardadfærd kan du bruge readResolve () metode til at tvinge strømmen til at returnere et passende objekt i stedet for det, der blev serieliseret. Heri særlig I det tilfælde er det relevante objekt det samme, som blev serieliseret. Her er et eksempel på, hvordan du implementerer readResolve () metode. Du kan finde ud af mere om readResolve () samt andre detaljer om serialisering på Suns websted dedikeret til Java Object Serialization Specification (se ressourcer).

En sidste gotcha at passe på er tilfældet med forbigående variabler. Hvis en variabel er markeret som forbigående, bliver den ikke serialiseret, og derfor kopieres den og dens graf ikke. I stedet vil værdien af ​​den forbigående variabel i det nye objekt være Java-sprogstandarderne (null, false og zero). Der er ingen kompilerings- eller runtime-fejl, hvilket kan resultere i adfærd, der er svær at fejle. Bare det at være opmærksom på dette kan spare meget tid.

Den dybe kopiteknik kan spare en programmør for mange timers arbejde, men kan forårsage de problemer, der er beskrevet ovenfor. Som altid skal du afveje fordele og ulemper, før du beslutter dig for, hvilken metode du skal bruge.

Konklusion

Implementering af dyb kopi af en kompleks objektgraf kan være en vanskelig opgave. Ovenstående teknik er et simpelt alternativ til den konventionelle procedure til overskrivning af klon () metode til hvert objekt i grafen.

Dave Miller er seniorarkitekt hos konsulentfirmaet Javelin Technology, hvor han arbejder med Java- og internetapplikationer. Han har arbejdet for virksomheder som Hughes, IBM, Nortel og MCIWorldcom på objektorienterede projekter og har udelukkende arbejdet med Java i de sidste tre år.

Lær mere om dette emne

  • Suns Java-websted har et afsnit dedikeret til Java Object Serialization Specification

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

Denne historie, "Java Tip 76: Et alternativ til den dybe kopiteknik" blev oprindeligt udgivet af JavaWorld.