Programmering

Java Tips 76: Et alternativ til deep copy-teknikken

Å implementere en dyp kopi av et objekt kan være en læringsopplevelse - du lærer at du ikke vil gjøre det! Hvis det aktuelle objektet refererer til andre komplekse objekter, som igjen refererer til andre, kan denne oppgaven faktisk være skremmende. Tradisjonelt må hver klasse i objektet inspiseres og redigeres individuelt for å implementere Klonbar grensesnitt og overstyre dens klone () metode for å lage en dyp kopi av seg selv så vel som dens inneholdte objekter. Denne artikkelen beskriver en enkel teknikk å bruke i stedet for denne tidkrevende konvensjonelle dypkopien.

Konseptet med dyp kopi

For å forstå hva en dyp kopi er, la oss først se på konseptet med grunne kopiering.

I en tidligere JavaWorld artikkel, "Hvordan unngå feller og korrekt overstyre metoder fra java.lang.Object," forklarer Mark Roulo hvordan man kloner objekter, samt hvordan man oppnår grunne kopiering i stedet for dypkopiering. For å oppsummere kort her, oppstår en grunne kopi når et objekt kopieres uten dets innhold. For å illustrere, viser figur 1 et objekt, obj1, som inneholder to objekter, inneholdtObj1 og inneholdtObj2.

Hvis det blir utført en grunne kopi på obj1, så kopieres den, men dens innholdsobjekter er ikke, som vist i figur 2.

En dyp kopi oppstår når et objekt kopieres sammen med objektene det refererer til. Figur 3 viser obj1 etter at en dyp kopi er utført på den. Ikke bare har obj1 blitt kopiert, men gjenstandene i den er også kopiert.

Hvis noen av disse inneholdte objektene selv inneholder objekter, blir disse objektene også kopiert i en dyp kopi, og så videre til hele grafen er krysset og kopiert. Hvert objekt er ansvarlig for å klone seg selv via sitt klone () metode. Standaren klone () metode, arvet fra Gjenstand, lager en grunne kopi av objektet. For å oppnå en dyp kopi, må ekstra logikk legges til som eksplisitt kaller alle inneholdte objekter ' klone () metoder, som igjen kaller deres inneholdte objekter ' klone () metoder, og så videre. Å få dette riktig kan være vanskelig og tidkrevende, og er sjelden morsomt. For å gjøre ting enda mer komplisert, hvis et objekt ikke kan modifiseres direkte og dets klone () metoden produserer en grunne kopi, så må klassen utvides, klone () metoden overstyrt, og denne nye klassen brukes i stedet for den gamle. (For eksempel, Vector inneholder ikke den logikken som er nødvendig for en dyp kopi.) Og hvis du vil skrive kode som avviger til kjøretiden spørsmålet om du skal lage en dyp eller grunne kopi av et objekt, er du inne for en enda mer komplisert situasjon. I dette tilfellet må det være to kopifunksjoner for hvert objekt: en for en dyp kopi og en for en grunne. Til slutt, selv om objektet som blir dypt kopiert inneholder flere referanser til et annet objekt, bør sistnevnte objekt fortsatt bare kopieres en gang. Dette forhindrer spredning av objekter, og avviker den spesielle situasjonen der en sirkulær referanse produserer en uendelig løkke med kopier.

Serialisering

Tilbake i januar 1998, JavaWorld innledet sin JavaBeans spalte av Mark Johnson med en artikkel om serialisering, "Gjør det på 'Nescafé' måte - med frysetørkede JavaBeans." For å oppsummere er serialisering muligheten til å gjøre en graf med objekter (inkludert degenerert tilfelle av et enkelt objekt) til en rekke byte som kan gjøres om til en tilsvarende graf med objekter. Et objekt sies å kunne serienummeres hvis det eller en av forfedrene implementerer java.io Serialiserbar eller java.io. kan utvides. Et serialiserbart objekt kan serieiseres ved å sende det til writeObject () metode for en ObjectOutputStream gjenstand. Dette skriver ut objektets primitive datatyper, matriser, strenger og andre objektreferanser. De writeObject () metoden kalles deretter på de henviste objektene for å serieisere dem også. Videre har hver av disse objektene deres referanser og gjenstander seriell; denne prosessen fortsetter og fortsetter til hele grafen er krysset og seriellisert. Høres dette kjent ut? Denne funksjonaliteten kan brukes til å oppnå en dyp kopi.

Dyp kopi ved hjelp av serialisering

Trinnene for å lage en dyp kopi ved hjelp av serialisering er:

  1. Forsikre deg om at alle klassene i objektets graf kan serienummeres.

  2. Lag inngangs- og utgangsstrømmer.

  3. Bruk inngangs- og utgangsstrømmene til å lage inn- og utgangsstrømmer for objekt.

  4. Send objektet du vil kopiere til objektets utgangsstrøm.

  5. Les det nye objektet fra objektets inngangsstrøm og kast det tilbake til klassen til objektet du sendte.

Jeg har skrevet en klasse som heter ObjectCloner som implementerer trinn to til fem. Linjen merket "A" setter opp a ByteArrayOutputStream som brukes til å lage ObjectOutputStream på linje B. Linje C er der magien gjøres. De writeObject () metoden krysser objektets graf rekursivt, genererer et nytt objekt i byteform og sender det til ByteArrayOutputStream. Linje D sørger for at hele objektet er sendt. Koden på linje E oppretter deretter en ByteArrayInputStream og fyller den med innholdet i ByteArrayOutputStream. Linje F instantierer en ObjectInputStream bruker ByteArrayInputStream opprettet på linje E og objektet blir deserialisert og returnert til anropsmetoden på linje G. Her er koden:

importer java.io. *; importer java.util. *; importer java.awt. *; offentlig klasse ObjectCloner {// slik at ingen ved et uhell kan opprette et ObjectCloner-objekt privat ObjectCloner () {} // returnerer en dyp kopi av et objekt statisk offentlig Object deepCopy (Object oldObj) kaster Unntak {ObjectOutputStream oos = null; ObjectInputStream ois = null; prøv {ByteArrayOutputStream bos = ny ByteArrayOutputStream (); // A oos = ny ObjectOutputStream (bos); // B // serialisere og sende objektet oos.writeObject (oldObj); // C oos.flush (); // D ByteArrayInputStream bin = ny ByteArrayInputStream (bos.toByteArray ()); // E ois = ny ObjectInputStream (bin); // F // returner det nye objektet returner ois.readObject (); // G} catch (Unntak e) {System.out.println ("Unntak i ObjectCloner =" + e); kaste (e); } til slutt {oos.close (); ois.close (); }}} 

Alt en utvikler med tilgang til ObjectCloner er igjen å gjøre før du kjører denne koden er å sikre at alle klassene i objektets graf kan serienummeres. I de fleste tilfeller burde dette allerede vært gjort; hvis ikke, burde det være relativt enkelt å gjøre med tilgang til kildekoden. De fleste klassene i JDK kan serienummereres; bare de som er plattformavhengige, for eksempel FileDescriptor, er ikke. Eventuelle klasser du får fra en tredjepartsleverandør som er JavaBean-kompatible, kan per definisjon også serienummereres. Hvis du utvider en klasse som kan serialiseres, kan den nye klassen selvfølgelig også serienummeres. Med alle disse klassifiserbare klassene som flyter rundt, er sjansen stor for at de eneste du kanskje trenger å serialisere er dine egne, og dette er et stykke kake i forhold til å gå gjennom hver klasse og overskrive klone () å lage en dyp kopi.

En enkel måte å finne ut om du har noen klassifiseringer som ikke kan deles om i objektets graf, er å anta at de alle kan serieiseres og kjøres ObjectCloners deepCopy () metode på den. Hvis det er et objekt hvis klasse ikke kan serieiseres, så a java.io.NotSerializableException vil bli kastet og fortelle deg hvilken klasse som forårsaket problemet.

Et raskt eksempel på implementering er vist nedenfor. Det skaper et enkelt objekt, v1, hvilken er en Vector som inneholder en Punkt. Dette objektet skrives deretter ut for å vise innholdet. Den opprinnelige gjenstanden, v1, blir deretter kopiert til et nytt objekt, vNytt, som skrives ut for å vise at den inneholder samme verdi som v1. Deretter innholdet i v1 endres, og til slutt begge deler v1 og vNytt blir skrevet ut slik at verdiene deres kan sammenlignes.

importer java.util. *; importer 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]; } annet {System.out.println ("Bruk: java Driver1 [dyp, grunne" "); komme tilbake; } // opprett originalobjekt Vector v1 = ny Vector (); Punkt p1 = nytt punkt (1,1); v1.addElement (p1); // se hva det er System.out.println ("Original =" + v1); Vector vNy = null; hvis (meth.equals ("deep")) {// deep copy vNew = (Vector) (ObjectCloner.deepCopy (v1)); // A} annet hvis (meth.equals ("grunt")) {// grunt eksemplar vNew = (Vector) v1.clone (); // B} // bekreft at det er det samme System.out.println ("New =" + vNew); // endre innholdet på det opprinnelige objektet p1.x = 2; p1.y = 2; // se hva som er i hver nå System.out.println ("Original =" + v1); System.out.println ("Ny =" + vNy); } fange (Unntak e) {System.out.println ("Unntak i hoved =" + e); }}} 

For å påkalle den dype kopien (linje A), kjør java.exe Driver1 dyp. Når den dype kopien kjører, får vi følgende utskrift:

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, ble endret, den nye Punkt opprettet som et resultat av den dype kopien forble upåvirket siden hele grafen ble kopiert. Til sammenligning kan du påkalle den grunne kopien (linje B) ved å utføre den java.exe Driver1 grunne. Når den grunne kopien kjører, får vi følgende utskrift:

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 ble endret, den nye Punkt ble endret også. Dette skyldes det faktum at den grunne kopien bare kopierer referansene, og ikke av objektene de henviser til. Dette er et veldig enkelt eksempel, men jeg tror det illustrerer poenget.

Implementeringsspørsmål

Nå som jeg har forkynt om alle fordelene ved dyp kopiering ved hjelp av serialisering, la oss se på noen ting å passe på.

Den første problematiske saken er en klasse som ikke kan serieiseres og som ikke kan redigeres. Dette kan for eksempel skje hvis du bruker en tredjepartsklasse som ikke følger med kildekoden. I dette tilfellet kan du utvide den, lage den utvidede klassen Serialiserbar, legg til eventuelle (eller alle) nødvendige konstruktører som bare kaller den tilknyttede superkonstruktøren, og bruk denne nye klassen overalt hvor du gjorde den gamle (her er et eksempel på dette).

Dette kan virke som mye arbeid, men med mindre den opprinnelige klassen klone () metoden implementerer dyp kopi, vil du gjøre noe lignende for å overstyre dens klone () metoden uansett.

Neste utgave er kjøretidshastigheten til denne teknikken. Som du kan forestille deg, er det sakte å opprette en stikkontakt, serieisere et objekt, sende det gjennom stikkontakten og deserialisere det, sammenlignet med anropsmetoder i eksisterende objekter. Her er noen kildekoder som måler tiden det tar å gjøre begge dypkopimetoder (via serialisering og klone ()) på noen enkle klasser, og produserer referanser for forskjellige antall iterasjoner. Resultatene, vist i millisekunder, er i tabellen nedenfor:

Millisekunder for å dype kopiere en enkel klassegraf n ganger
Prosedyre \ Iterasjoner (n)100010000100000
klone10101791
serialisering183211346107725

Som du ser er det stor forskjell i ytelse. Hvis koden du skriver er ytelseskritisk, kan det hende du må bite i kulen og håndkode en dyp kopi. Hvis du har en kompleks graf og får en dag til å implementere en dyp kopi, og koden kjøres som en batchjobb klokken ett om morgenen på søndager, gir denne teknikken deg et annet alternativ å vurdere.

Et annet spørsmål er å behandle saken om en klasse hvis objektenes forekomster i en virtuell maskin må kontrolleres. Dette er et spesielt tilfelle av Singleton-mønsteret, der en klasse bare har ett objekt i en VM. Som diskutert ovenfor, når du serierer et objekt, oppretter du et helt nytt objekt som ikke vil være unikt. For å omgå denne standardadferden kan du bruke readResolve () metode for å tvinge strømmen til å returnere et passende objekt i stedet for det som ble seriellisert. I dette bestemt i tilfelle er det riktige objektet det samme som ble seriellisert. Her er et eksempel på hvordan du implementerer readResolve () metode. Du kan finne ut mer om readResolve () i tillegg til andre detaljer om serialisering på Suns nettsted dedikert til Java Object Serialization Specification (se Ressurser).

En siste må være oppmerksom på er tilfellet med forbigående variabler. Hvis en variabel er merket som forbigående, blir den ikke seriellisert, og derfor blir den og dens graf ikke kopiert. I stedet vil verdien av den forbigående variabelen i det nye objektet være Java-språkstandardene (null, usann og null). Det vil ikke være noen kompilerings- eller kjøretidsfeil, noe som kan føre til atferd som er vanskelig å feilsøke. Bare det å være klar over dette kan spare mye tid.

Deep copy-teknikken kan spare en programmerer mange timer med arbeid, men kan forårsake problemene beskrevet ovenfor. Som alltid må du veie fordeler og ulemper før du bestemmer deg for hvilken metode du vil bruke.

Konklusjon

Implementering av dyp kopi av en kompleks objektgraf kan være en vanskelig oppgave. Teknikken vist ovenfor er et enkelt alternativ til den konvensjonelle prosedyren for overskriving av klone () metode for hvert objekt i grafen.

Dave Miller er seniorarkitekt hos konsulentfirmaet Javelin Technology, hvor han jobber med Java og Internett-applikasjoner. Han har jobbet for selskaper som Hughes, IBM, Nortel og MCIWorldcom med objektorienterte prosjekter, og har jobbet eksklusivt med Java de siste tre årene.

Lær mer om dette emnet

  • Suns Java-nettsted har en seksjon dedikert til Java Object Serialization Specification

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

Denne historien, "Java Tip 76: An alternative to the deep copy technique" ble opprinnelig utgitt av JavaWorld.

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