Programavimas

„Java“ patarimas 76: giliosios kopijos technikos alternatyva

Gilios objekto kopijos įgyvendinimas gali būti mokymosi patirtis - sužinosite, kad nenorite to daryti! Jei nagrinėjamas objektas nurodo kitus sudėtingus objektus, kurie savo ruožtu nurodo kitus, ši užduotis išties gali būti bauginanti. Tradiciškai kiekviena objekto klasė turi būti atskirai tikrinama ir redaguojama, kad būtų įgyvendinta Klonuojamas sąsają ir nepaisyti jos klonas () metodą, kad būtų galima giliai kopijuoti save ir jame esančius objektus. Šiame straipsnyje aprašoma paprasta technika, naudojama vietoj šios daug laiko reikalaujančios gilios kopijos.

Gilios kopijos samprata

Norint suprasti, ką a gili kopija tai pirmiausia pažiūrėkime į negilaus kopijavimo sąvoką.

Ankstesniame „JavaWorld“ straipsnis „Kaip išvengti spąstų ir teisingai nepaisyti metodų iš java.lang.Object“, Markas Roulo paaiškina, kaip klonuoti objektus, taip pat kaip pasiekti negilų, o ne gilų kopijavimą. Trumpai apibendrinant galima pasakyti, kad negiliai kopijuojama, kai objektas nukopijuojamas be jame esančių objektų. 1 paveiksle pavaizduotas objektas, obj1, kuriame yra du objektai, yraObj1 ir sudėtyjeObj2.

Jei atliekama negili kopija obj1, tada jis nukopijuojamas, bet jame esantys objektai nėra, kaip parodyta 2 paveiksle.

Gilioji kopija įvyksta, kai objektas nukopijuojamas kartu su objektais, į kuriuos jis nurodo. 3 paveiksle parodyta obj1 atlikus gilią jo kopiją. Ne tik turi obj1 buvo nukopijuoti, tačiau joje esantys objektai taip pat nukopijuoti.

Jei bet kuriame iš šių esančių objektų yra objektų, tada giluminėje kopijoje tie objektai taip pat nukopijuojami ir taip toliau, kol bus pereita ir nukopijuota visa diagrama. Kiekvienas objektas yra atsakingas už savęs klonavimą klonas () metodas. Numatytasis klonas () metodas, paveldėtas iš Objektas, padaro negilią objekto kopiją. Norint gauti išsamią kopiją, reikia pridėti papildomą logiką, kuri aiškiai vadina visus esančius objektus ' klonas () metodus, kurie savo ruožtu vadina jų turimus objektus “ klonas () metodai ir pan. Tai padaryti gali būti sunku ir daug laiko reikalaujanti teisinga praktika. Tai retai būna smagu. Kad viskas būtų dar sudėtingiau, jei objekto negalima tiesiogiai modifikuoti ir jo klonas () metodas sukuria negilų egzempliorių, tada klasė turi būti išplėsta, klonas () metodas atmetamas, o ši nauja klasė naudojama vietoj senosios. (Pavyzdžiui, Vektorius nėra logikos, reikalingos giliajai kopijai.) Ir jei norite parašyti kodą, kuris iki vykdymo atideda klausimą, ar padaryti gilų, ar negilų objekto kopijavimą, esate dar sudėtingesnėje situacijoje. Tokiu atveju kiekvienam objektui turi būti dvi kopijavimo funkcijos: viena - giliajai kopijai ir viena - negiliai. Galiausiai, net jei giliai kopijuojamame objekte yra kelios nuorodos į kitą objektą, pastarąjį objektą vis tiek reikėtų nukopijuoti tik vieną kartą. Tai užkerta kelią objektų dauginimuisi ir pašalina ypatingą situaciją, kai žiedinė nuoroda sukuria begalinę kopijų kilpą.

Serializavimas

1998 m. Sausio mėn. „JavaWorld“ inicijavo savo „JavaBeans“ Marko Johnsono skiltis su straipsniu apie serializaciją „Daryk tai„ Nescafé “būdu - su liofilizuotais„ JavaBeans “. Apibendrinant galima pasakyti, kad serializavimas yra galimybė objektų grafiką (įskaitant išskaidytą vieno objekto atvejį) paversti baitų masyvu, kurį galima vėl paversti lygiaverčiu objektų grafiku. Sakoma, kad objektas gali būti serijinis, jei jis ar vienas iš jo protėvių įgyvendina java.io.Serializuojama arba java.io.Išskirtinis. Serializuojamą objektą galima serializuoti perduodant jį writeObject () metodas ObjectOutputStream objektas. Tai užrašo pirminius objekto duomenų tipus, masyvus, eilutes ir kitas objekto nuorodas. writeObject () metodas, tada iškviesti objektai taip pat turi juos nuoseklinti. Be to, kiekvienas iš šių objektų turi nuorodos ir objektai serijiniai; šis procesas tęsiasi ir tęsiasi tol, kol visas grafikas bus pereinamas ir nuoseklus. Ar tai skamba pažįstamai? Šią funkciją galima naudoti norint gauti gilią kopiją.

Gili kopija, naudojant serijinę tvarką

Gilios kopijos kūrimo naudojant serijizavimą žingsniai yra šie:

  1. Įsitikinkite, kad visos objekto diagramos klasės yra serijinės.

  2. Kurkite įvesties ir išvesties srautus.

  3. Norėdami sukurti objekto įvesties ir išvesties srautus, naudokite įvesties ir išvesties srautus.

  4. Perkelkite norimą nukopijuoti objektą į objekto išvesties srautą.

  5. Perskaitykite naują objektą iš objekto įvesties srauto ir grąžinkite jį į išsiųsto objekto klasę.

Aš parašiau klasę, vadinamą „ObjectCloner“ kad įgyvendina du – penkis žingsnius. Linija, pažymėta "A", nustato a „ByteArrayOutputStream“ kuri naudojama kuriant ObjectOutputStream C linijoje yra magija. writeObject () metodas rekursyviai pereina objekto grafiką, sukuria naują objektą baito pavidalu ir siunčia jį į „ByteArrayOutputStream“. D eilutė užtikrina, kad visas objektas buvo išsiųstas. E eilutėje esantis kodas sukuria a „ByteArrayInputStream“ ir užpildo jį turiniu „ByteArrayOutputStream“. F eilutė momentuoja an ObjectInputStream naudojant „ByteArrayInputStream“ sukurtas E eilutėje, o objektas deserializuojamas ir grąžinamas į skambinimo metodą G eilutėje. Štai kodas:

importuoti java.io. *; importuoti java.util. *; importuoti java.awt. *; public class ObjectCloner {// kad niekas netyčia negalėtų sukurti objekto ObjectCloner private ObjectCloner () {} // grąžina gilią objekto kopiją statinė viešoji Object deepCopy (Object oldObj) meta išimtį {ObjectOutputStream oos = null; ObjectInputStream ois = nulis; pabandykite {ByteArrayOutputStream bos = new ByteArrayOutputStream (); // A oos = new ObjectOutputStream (bos); // B // serializuokite ir perduokite objektą oos.writeObject (oldObj); // C oos.flush (); // D ByteArrayInputStream bin = new ByteArrayInputStream (bos.toByteArray ()); // E ois = new ObjectInputStream (bin); // F // grąžina naują objektą return ois.readObject (); // G} gaudymas (išimtis e) {System.out.println ("Išimtis„ ObjectCloner “=" + e); metimas (e); } pagaliau {oos.close (); ois.uždaryti (); }}} 

Visi kūrėjai, turintys prieigą prie „ObjectCloner“ prieš paleidžiant šį kodą belieka įsitikinti, kad visos objekto grafiko klasės yra serijinės. Daugeliu atvejų tai turėjo būti padaryta jau dabar; jei ne, tai turėtų būti gana lengva padaryti su prieiga prie šaltinio kodo. Dauguma JDK klasių yra serijinės; tik tie, kurie priklauso nuo platformos, pvz FileDescriptor, nėra. Be to, visos klasės, kurias gaunate iš trečiosios šalies tiekėjo ir kurios atitinka „JavaBean“, pagal apibrėžimą yra serijinės. Žinoma, jei išplėsite klasę, kurią galima serijiniu būdu naudoti, naujoji klasė taip pat bus serijinė. Visoms šioms serializuojamoms klasėms plaukiant, yra tikimybė, kad vieninteliai, kuriuos jums gali tekti serializuoti, yra jūsų pačių, ir tai yra pyrago gabalas, palyginti su kiekvienos klasės perėjimu ir perrašymu klonas () padaryti gilią kopiją.

Paprastas būdas sužinoti, ar objekto diagramoje yra neserializuojamų klasių, yra manyti, kad jos visos yra serijinės ir vykdomos „ObjectCloner“'s „deepCopy“ () metodas. Jei yra objektas, kurio klasė negali būti serijinė, tada a java.io.NotSerializableException bus išmestas, nurodant, kuri klasė sukėlė problemą.

Greito įgyvendinimo pavyzdys parodytas žemiau. Tai sukuria paprastą objektą, v1, kuris yra a Vektorius kuriame yra a Taškas. Tada šis objektas atspausdinamas, kad būtų parodytas jo turinys. Originalus objektas, v1, tada nukopijuojamas į naują objektą, vNew, kuris atspausdintas, kad parodytų, jog jame yra ta pati reikšmė kaip v1. Kitas, turinys v1 yra pakeisti, ir galiausiai abu v1 ir vNew yra atspausdinti, kad būtų galima palyginti jų vertes.

importuoti java.util. *; importuoti java.awt. *; public class Driver1 {static public void main (String [] args) {try {// gauti metodą iš komandinės eilutės String meth; jei ((args.length == 1) && ((args [0]. lygi ("giliai")) || (args [0]. lygi ("sekli")))) {meth = args [0]; } else {System.out.println ("Naudojimas: java Driver1 [gilus, seklus]"); grįžti; } // sukurti originalų objektą Vector v1 = new Vector (); Taškas p1 = naujas taškas (1,1); v1.addElement (p1); // pamatyti, kas tai yra System.out.println ("Original =" + v1); Vektorius vNew = nulis; if (meth.equals ("gilus")) {// gilia kopija vNew = (Vector) (ObjectCloner.deepCopy (v1)); // A} else if (meth.equals ("sekli")) {// sekli kopija vNew = (Vector) v1.clone (); // B} // patikrinkite, ar tai tas pats System.out.println ("Naujas =" + vNew); // pakeisti pradinio objekto turinį p1.x = 2; p1.y = 2; // pamatyti, kas kiekviename dabar yra System.out.println ("Original =" + v1); System.out.println ("Naujas =" + vNew); } gaudyti (išimtis e) {System.out.println ("Išimtis pagrindiniame =" + e); }}} 

Norėdami iškviesti giliąją kopiją (A eilutė), vykdykite java.exe Driver1 giliai. Kai paleidžiama gilioji kopija, gauname tokį spaudinį:

Originalas = [java.awt.Point [x = 1, y = 1]] Naujas = [java.awt.Point [x = 1, y = 1]] Originalas = [java.awt.Point [x = 2, y = 2]] Nauja = [java.awt.Point [x = 1, y = 1]] 

Tai rodo, kad kai originalas Taškas, p1, buvo pakeista, nauja Taškas sukurta kaip gilioji kopija liko nepaveikta, nes buvo nukopijuotas visas grafikas. Norėdami palyginti, iškvieskite seklią kopiją (B eilutė) vykdydami java.exe Driver1 sekli. Kai sekli kopija paleista, gauname tokį spaudinį:

Originalas = [java.awt.Point [x = 1, y = 1]] Nauja = [java.awt.Point [x = 1, y = 1]] Originalas = [java.awt.Point [x = 2, y = 2]] Nauja = [java.awt.Point [x = 2, y = 2]] 

Tai rodo, kad kai originalas Taškas buvo pakeistas, naujas Taškas buvo pakeista taip pat. Taip yra dėl to, kad sekli kopija daro tik nuorodų, o ne objektų, į kuriuos jos nukreiptos, kopijas. Tai labai paprastas pavyzdys, bet manau, kad jis iliustruoja, hm, tašką.

Įgyvendinimo klausimai

Dabar, kai naudodama serializavimą pamokslavau apie visas giliosios kopijos dorybes, pažvelkime į keletą dalykų, į kuriuos reikia atkreipti dėmesį.

Pirmasis probleminis atvejis yra klasė, kurios negalima serializuoti ir kurios negalima redaguoti. Tai gali atsitikti, pavyzdžiui, jei naudojate trečiosios šalies klasę, kuri nėra pridėta prie šaltinio kodo. Tokiu atveju galite jį išplėsti, pritaikyti išplėstinę klasę Serijinis, pridėkite visus (arba visus) reikalingus konstruktorius, kurie tiesiog paskambins susijusiam superkonstruktoriui, ir naudokite šią naują klasę visur, kur darėte senąją (čia yra to pavyzdys).

Tai gali atrodyti kaip didelis darbas, bet nebent originalios klasės klonas () metodas įgyvendina gilią kopiją, darysite kažką panašaus, kad nepaisytumėte jos klonas () metodas vistiek.

Kitas klausimas yra šios technikos vykdymo laikas. Kaip galite įsivaizduoti, lizdo sukūrimas, objekto nuoseklinimas, perdavimas per lizdą ir deserializavimas yra lėtas, palyginti su esamų objektų iškvietimo metodais. Čia yra keli šaltinio kodai, rodantys laiką, kurio reikia norint atlikti abu giliosios kopijos metodus (naudojant serijinį ir klonas ()) kai kuriose paprastose klasėse ir sukuria skirtingo skaičiaus pakartojimų etalonus. Rezultatai, pateikiami milisekundėmis, pateikti toliau pateiktoje lentelėje:

Milisekundėmis giliai nukopijuokite paprastą klasės grafiką n kartų
Procedūra \ kartojimai (n)100010000100000
klonas10101791
serializavimas183211346107725

Kaip matote, yra didelis našumo skirtumas. Jei jūsų rašomas kodas yra labai svarbus našumui, jums gali tekti įkąsti kulka ir rankiniu būdu užkoduoti gilią kopiją. Jei turite sudėtingą grafiką ir jums suteikiama viena diena, kad būtų įdiegta gilioji kopija, o kodas bus vykdomas kaip paketinis darbas sekmadienio vieną rytą, tada ši technika suteikia jums dar vieną galimybę apsvarstyti.

Kitas klausimas susijęs su klasės, kurios objektų egzemplioriai virtualioje mašinoje turi būti valdomi, atveju. Tai yra ypatingas „Singleton“ modelio atvejis, kai klasėje VM yra tik vienas objektas. Kaip aptarta aukščiau, kai nuosekliai suremontuojate objektą, sukuriate visiškai naują objektą, kuris nebus unikalus. Norėdami apeiti šį numatytąjį elgesį, galite naudoti readResolve () metodas priversti srautą grąžinti tinkamą objektą, o ne tą, kuris buvo serijinis. Šiame ypač Atitinkamas objektas yra tas pats, kuris buvo serijinis. Čia yra pavyzdys, kaip įgyvendinti readResolve () metodas. Galite sužinoti daugiau apie readResolve () taip pat kita išsami informacija apie „Sun“ svetainę, skirtą „Java“ objektų serializacijos specifikacijai (žr. Ištekliai).

Paskutinė problema, į kurią reikia atkreipti dėmesį, yra laikinų kintamųjų atvejis. Jei kintamasis bus pažymėtas kaip trumpalaikis, jis nebus nuoseklus, todėl jis ir jo grafikas nebus nukopijuoti. Vietoj to, pereinamojo kintamojo vertė naujame objekte bus numatytoji „Java“ kalbos reikšmė („null“, „false“ ir „zero“). Nebus kompiliavimo ar vykdymo laiko klaidų, dėl kurių gali būti sunku derinti. Vien tai žinant galima sutaupyti daug laiko.

Giliosios kopijos technika gali sutaupyti programuotojui daug darbo valandų, tačiau gali sukelti aukščiau aprašytas problemas. Kaip visada, prieš nuspręsdami, kurį metodą naudoti, būtinai pasverkite pranašumus ir trūkumus.

Išvada

Gilios sudėtingo objekto grafiko kopijos įgyvendinimas gali būti sudėtinga užduotis. Aukščiau parodyta technika yra paprasta alternatyva įprastai procedūrai perrašyti klonas () metodas kiekvienam grafiko objektui.

Dave'as Milleris yra vyresnysis architektas konsultacijų firmoje „Javelin Technology“, kur dirba su „Java“ ir interneto programomis. Jis dirbo tokiose įmonėse kaip „Hughes“, „IBM“, „Nortel“ ir „MCIWorldcom“ vykdydamas objektinius projektus, o pastaruosius trejus metus dirbo tik su „Java“.

Sužinokite daugiau apie šią temą

  • „Sun“ „Java“ svetainėje yra skyrius, skirtas „Java“ objektų serizavimo specifikacijai

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

Šią istoriją „Java Patarimas 76: Gilaus kopijavimo technikos alternatyva“ iš pradžių paskelbė „JavaWorld“.