Kako napraviti duboku kopiju objekta na Javi

1. Uvod

Kada želimo kopirati objekt u Javi, dvije su mogućnosti koje moramo uzeti u obzir - plitka kopija i dubinska kopija.

Plitka kopija je pristup kada kopiramo samo vrijednosti polja i stoga kopija može ovisiti o izvornom objektu. U pristupu dubinske kopije osiguravamo da su svi objekti u stablu duboko kopirani, tako da kopija ne ovisi o bilo kojem ranije postojećem objektu koji bi se ikad mogao promijeniti.

U ovom ćemo članku usporediti ova dva pristupa i naučiti četiri metode za implementaciju dubinske kopije.

2. Postavljanje Mavena

Upotrijebit ćemo tri Mavenove ovisnosti - Gson, Jackson i Apache Commons Lang - za testiranje različitih načina izvođenja dubinske kopije.

Dodajmo ove ovisnosti u našu pom.xml:

 com.google.code.gson gson 2.8.2 commons-lang commons-lang 2.6 com.fasterxml.jackson.core jackson-databind 2.9.3 

Najnovije verzije Gson, Jackson i Apache Commons Lang mogu se naći na Maven Central.

3. Model

Da bismo usporedili različite metode kopiranja Java objekata, trebat će nam dvije klase na kojima ćemo raditi:

klasa Adresa {private String street; privatni gudački grad; privatna gudačka zemlja; // standardni konstruktori, getteri i postavljači}
klasa Korisnik {private String firstName; private String lastName; adresa privatne adrese; // standardni konstruktori, getteri i postavljači}

4. Plitka kopija

Plitka kopija je ona u kojoj kopiramo samo vrijednosti polja s jednog objekta na drugi:

@Test javna void whenShallowCopying_thenObjectsShouldNotBeSame () {Adresa adresa = nova adresa ("Downing St 10", "London", "England"); Korisnik pm = novi Korisnik ("Prime", "Minister", adresa); Korisnik shallowCopy = novi korisnik (pm.getFirstName (), pm.getLastName (), pm.getAddress ()); assertThat (shallowCopy) .isNotSameAs (pm); }

U ovom slučaju pm! = shallowCopy, što znači da oni su različiti objekti, ali problem je u tome što kad promijenimo bilo koji od originala adresa' svojstva, to će također utjecati na plitkokopirajAdresa.

Ne bismo se oko toga trudili da Adresa bio nepromjenjiv, ali nije:

@Test public void whenModifyingOriginalObject_ThenCopyShouldChange () {Adresa adresa = nova adresa ("Downing St 10", "London", "England"); Korisnik pm = novi Korisnik ("Prime", "Minister", adresa); Korisnik shallowCopy = novi korisnik (pm.getFirstName (), pm.getLastName (), pm.getAddress ()); address.setCountry ("Velika Britanija"); assertThat (shallowCopy.getAddress (). getCountry ()) .isEqualTo (pm.getAddress (). getCountry ()); }

5. Duboka kopija

Dubinska kopija je alternativa koja rješava ovaj problem. Njegova je prednost barem u tome svaki se promjenjivi objekt u grafu predmeta rekurzivno kopira.

Budući da kopija ne ovisi ni o jednom promjenjivom objektu koji je stvoren ranije, neće se slučajno izmijeniti kao što smo vidjeli kod plitke kopije.

U sljedećim odjeljcima prikazat ćemo nekoliko implementacija dubinskih kopija i demonstrirati tu prednost.

5.1. Konstruktor kopija

Prva implementacija koju ćemo implementirati temelji se na konstruktorima kopija:

javna adresa (Adresa koja) {this (that.getStreet (), that.getCity (), that.getCountry ()); }
javni korisnik (User that) {this (that.getFirstName (), that.getLastName (), nova adresa (that.getAddress ())); }

U gornjoj implementaciji dubinske kopije nismo stvorili novu Žice u našem konstruktoru kopija jer Niz je nepromjenjiva klasa.

Kao rezultat, ne mogu se slučajno modificirati. Da vidimo hoće li ovo uspjeti:

@Test public void whenModifyingOriginalObject_thenCopyShouldNotChange () {Adresa adrese = nova adresa ("Downing St 10", "London", "England"); Korisnik pm = novi Korisnik ("Prime", "Minister", adresa); Korisnik deepCopy = novi korisnik (pm); address.setCountry ("Velika Britanija"); assertNotEquals (pm.getAddress (). getCountry (), deepCopy.getAddress (). getCountry ()); }

5.2. Cloneable sučelje

Druga se implementacija temelji na metodi klona naslijeđenoj od Objekt. Zaštićen je, ali ga moramo nadjačati kao javnost.

Također ćemo dodati sučelje markera, Klonirano, razredima kako bi ukazali da su te klase zapravo moguće klonirati.

Dodajmo i klon() metoda za Adresa razred:

@Preuzmi javni objektni klon () {try {return (Adresa) super.clone (); } catch (CloneNotSupportedException e) {return new Address (this.street, this.getCity (), this.getCountry ()); }}

A sada provedimo klon() za Korisnik razred:

@Override javni objektni klon () {Korisnik = null; isprobajte {user = (User) super.clone (); } catch (CloneNotSupportedException e) {user = novi korisnik (this.getFirstName (), this.getLastName (), this.getAddress ()); } user.address = (Adresa) this.address.clone (); povratni korisnik; }

Imajte na umu da super.clone () call vraća plitku kopiju objekta, ali dubinske kopije promjenjivih polja postavljamo ručno, tako da je rezultat točan:

@Test public void whenModifyingOriginalObject_thenCloneCopyShouldNotChange () {Adresa adrese = nova adresa ("Downing St 10", "London", "England"); Korisnik pm = novi Korisnik ("Prime", "Minister", adresa); Korisnik deepCopy = (Korisnik) pm.clone (); address.setCountry ("Velika Britanija"); assertThat (deepCopy.getAddress (). getCountry ()) .isNotEqualTo (pm.getAddress (). getCountry ()); }

6. Vanjske knjižnice

Gornji primjeri izgledaju lako, ali ponekad se ne mogu primijeniti kao rješenje kada ne možemo dodati dodatni konstruktor ili nadjačati metodu kloniranja.

To se može dogoditi kada nismo u vlasništvu koda ili kada je graf objekta toliko kompliciran da ne bismo završili projekt na vrijeme ako bismo se usredotočili na pisanje dodatnih konstruktora ili implementaciju klon metoda na svim klasama u objektnom grafu.

Što onda? U ovom slučaju možemo koristiti vanjsku knjižnicu. Da biste postigli dubinsku kopiju, možemo serializirati objekt, a zatim ga deserializirati na novi objekt.

Pogledajmo nekoliko primjera.

6.1. Apache Commons Lang

Apache Commons Lang ima SerializationUtils # klon, koja izvodi dubinsku kopiju kada sve klase u objektnom grafikonu implementiraju Serijalizirati sučelje.

Ako metoda naiđe na klasu koja se ne može serializirati, neće uspjeti i bacit će neoznačeno SerializationException.

Zbog toga moramo dodati Serijalizirati sučelje za naše satove:

@Test public void whenModifyingOriginalObject_thenCommonsCloneShouldNotChange () {Adresa adresa = nova adresa ("Downing St 10", "London", "England"); Korisnik pm = novi Korisnik ("Prime", "Minister", adresa); Korisnik deepCopy = (Korisnik) SerializationUtils.clone (pm); address.setCountry ("Velika Britanija"); assertThat (deepCopy.getAddress (). getCountry ()) .isNotEqualTo (pm.getAddress (). getCountry ()); }

6.2. JSON serializacija s Gson

Drugi način serializacije je upotreba JSON serializacije. Gson je knjižnica koja se koristi za pretvaranje objekata u JSON i obrnuto.

Za razliku od Apache Commons Lang, GSON-u nije potreban Serijalizirati sučelje za pretvorbu.

Kratko ćemo pogledati primjer:

@Test public void whenModifyingOriginalObject_thenGsonCloneShouldNotChange () {Adresa adrese = nova adresa ("Downing St 10", "London", "England"); Korisnik pm = novi Korisnik ("Prime", "Minister", adresa); Gson gson = novi Gson (); Korisnik deepCopy = gson.fromJson (gson.toJson (pm), User.class); address.setCountry ("Velika Britanija"); assertThat (deepCopy.getAddress (). getCountry ()) .isNotEqualTo (pm.getAddress (). getCountry ()); }

6.3. JSON serializacija s Jacksonom

Jackson je još jedna knjižnica koja podržava JSON serializaciju. Ova će implementacija biti vrlo slična onoj koja koristi Gson, ali trebamo dodati zadani konstruktor u naše klase.

Pogledajmo primjer:

@Test public void whenModifyingOriginalObject_thenJacksonCopyShouldNotChange () baca IOException {Adresa adresa = nova adresa ("Downing St 10", "London", "England"); Korisnik pm = novi Korisnik ("Prime", "Minister", adresa); ObjectMapper objectMapper = novi ObjectMapper (); Korisnik deepCopy = objectMapper .readValue (objectMapper.writeValueAsString (pm), User.class); address.setCountry ("Velika Britanija"); assertThat (deepCopy.getAddress (). getCountry ()) .isNotEqualTo (pm.getAddress (). getCountry ()); }

7. Zaključak

Koju bismo implementaciju trebali koristiti prilikom izrade dubinske kopije? Konačna odluka često će ovisiti o razredima koje ćemo kopirati i o tome posjedujemo li klase u objektnom grafu.

Kao i uvijek, cjeloviti uzorci koda za ovu lekciju možete pronaći na GitHubu.