Duboko razumevanje plitkog i dubokog kopiranja u Javi
„Brate, čuo sam da su plitko i duboko kopiranje često postavljano pitanje na Java intervjuu, da li je tako?“
„Stvarno je tako, i razumevanje principa plitkog i dubokog kopiranja će dati i dublje razumevanje da li Java prenosi po vrednosti ili po referenci.“ Potvrdio sam.
„Bilo da je plitko ili duboko kopiranje, može se dovršiti pozivanjem clone() metode Object klase." Dok sam govorio, otvorio sam Intellij IDEA i pronašao izvorni kod clone() metode.
protected native Object clone() throws CloneNotSupportedException;Treba napomenuti da je clone() metoda ujedno i native metoda, njena konkretna implementacija se prepštuće HotSpot virtuelnoj mašini, to znači da virtuelna mašina pri izvršavanju ove metode zamenjuje je efikasnijim C/C++ kodom, zatim poziva operativni sistem da dovrši rad kloniranja objekta.
Nakon Java 9, ova metoda će biti označena sa
@HotSpotIntrinsicCandidateanotacijom, metode označene ovom anotacijom imaju efikasnu implementaciju u HotSpot virtuelnoj mašini.
„Brate, prvo reci o plitkom kopiranju!“
„Dobro.“ Idemo direktno u praktični kod.
class Writer implements Cloneable{
private int age;
private String name;
// getter/setter i konstruktori su izostavljeni
@Override
public String toString() {
return super.toString().substring(26) + "{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}Klasa Writer ima dva polja, int tipa age, i String tipa name. Zatim smo prepisali toString() metodu, pogodno za ispis konkretnih informacija o objektu.
„Zašto implementirati Cloneable interfejs?“ Sanmej je uključila režim 100.000 pitanja.
Cloneable interfejs je marker interfejs, unutar njega je prazan:
public interface Cloneable {
}Samo, ako klasa ne implementira Cloneable interfejs, iako je prepisala clone() metodu, i dalje ne može pozivati tu metodu za kloniranje objekta, program će pri izvršavanju clone() metode baciti CloneNotSupportedException izuzetak.
Exception in thread "main" java.lang.CloneNotSupportedExceptionUloga marker interfejsa je zapravo vrlo jednostavna, koristi se za označavanje da je izvršenje određene funkcije legitimno.
„Oh, razumemah!“ Sanmej izgleda da je potpuno razumela sadržaj koju sam rekao.
„Nastavimo, testna klasa."
class TestClone {
public static void main(String[] args) throws CloneNotSupportedException {
Writer writer1 = new Writer(18,"Erge");
Writer writer2 = (Writer) writer1.clone();
System.out.println("Posle plitkog kopiranja:");
System.out.println("writer1:" + writer1);
System.out.println("writer2:" + writer2);
writer2.setName("Sanmej");
System.out.println("Posle podešavanja writer2 imena na:");
System.out.println("writer1:" + writer1);
System.out.println("writer2:" + writer2);
}
}- Deklarisali smo Writer objekat (18-godišnji Erge) kroz
newključnu reč, dodelili ga writer1. - Klonirali smo objekat pozivanjem
clone()metode, dodelili ga writer2. - Zatim smo ispisali writer1 i writer2.
- Podesili smo polje name writer2 na „Sanmej".
- Ponovo ispisali.
Pogledaj rezultate.
Posle plitkog kopiranja:
writer1:Writer@68837a77{age=18, name='Erge'}
writer2:Writer@b97c004{age=18, name='Erge'}
Posle podešavanja writer2 imena na:
writer1:Writer@68837a77{age=18, name='Erge'}
writer2:Writer@b97c004{age=18, name='Sanmej'}Može se videti da posle plitkog kopiranja writer1 i writer2 referenciraju različite objekte, ali vrednosti su iste, što znači da je kopiranje uspešno. Zatim, nakon modifikacije polja name writer2, direktno slika će jasno pokazati.

U prethodnom primeru, klasa Writer ima samo dva polja, nema polja referentnog tipa. Zatim, pogledajmo još jedan primer, dodajemo jedno prilagođeno referentno polje Book klasi Writer, prvo pogledaj definiciju Book.
class Book {
private String bookName;
private int price;
// getter/setter i konstruktori su izostavljeni
@Override
public String toString() {
return super.toString().substring(26) +
" bookName='" + bookName + '\'' +
", price=" + price +
'}';
}
}Ima dva polja, String tipa bookName i int tipa price.
Zatim pogledaj definiciju Writer klase.
class Writer implements Cloneable{
private int age;
private String name;
private Book book;
// getter/setter i konstruktori su izostavljeni
@Override
public String toString() {
return super.toString().substring(26) +
" age=" + age +
", name='" + name + '\'' +
", book=" + book +
')';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}U poređenju sa prethodnim primerom, dodato je još jedno polje prilagođenog tipa book, clone() metoda se nije promenila.
Još jedan testni primer.
class TestClone {
public static void main(String[] args) throws CloneNotSupportedException {
Writer writer1 = new Writer(18,"Erge");
Book book1 = new Book("Principi kompajlera",100);
writer1.setBook(book1);
Writer writer2 = (Writer) writer1.clone();
System.out.println("Posle plitkog kopiranja:");
System.out.println("writer1:" + writer1);
System.out.println("writer2:" + writer2);
Book book2 = writer2.getBook();
book2.setBookName("Trajni Tjuring");
book2.setPrice(70);
System.out.println("Posle promene writer2.book:");
System.out.println("writer1:" + writer1);
System.out.println("writer2:" + writer2);
}
}- Deklarisali smo Writer objekat (18-godišnji Erge) kroz
newključnu reč, dodelili ga writer1. - Deklarisali smo Book objekat (100 vredni Principi kompajlera) kroz
newključnu reč, dodelili ga book1. - Dodelili smo book1 polju book writer1.
- Klonirali smo objekat pozivanjem
clone()metode, dodelili ga writer2. - Zatim smo ispisali writer1 i writer2.
- Dobavili smo polje book writer2, dodelili ga book2.
- Podesili smo polje bookName book2 na „Trajni Tjuring", polje price na 70.
- Ponovo ispisali.
Pogledaj rezultate.
Posle plitkog kopiranja:
writer1:Writer@68837a77 age=18, name='Erge', book=Book@32e6e9c3 bookName='Principi kompajlera', price=100}}
writer2:Writer@6d00a15d age=18, name='Erge', book=Book@32e6e9c3 bookName='Principi kompajlera', price=100}}
Posle promene writer2.book:
writer1:Writer@68837a77 age=18, name='Erge', book=Book@32e6e9c3 bookName='Trajni Tjuring', price=70}}
writer2:Writer@6d00a15d age=18, name='Erge', book=Book@32e6e9c3 bookName='Trajni Tjuring', price=70}}Razlika od prethodnog primera je u tome što se nakon promene writer2.book menja i writer1.book. To je zato što je String nepromenjiv objekat, nova vrednost mora otvoriti novi prostor u bazenu konstanti stringa, dok se memorijska adresa prilagođenog objekta nije promenila, samo su se promenile vrednosti odgovarajućih polja, pogledaj sliku.

„Va, brate, zaista jedna slika vredi više od hiljadu reči, razumem.“ Sanmej se izgleda zainteresovala za sliku koju sam nacrtao, „nastavi sa dubokim kopiranjem!“
„Mhm, Sanmej, primetila si li, kod plitkog kopiranja polja referentnog tipa kloniranog objekta pokazuju na isto, kada se menja jedan objekat, drugi objekat takođe prati promenu, osim specifičnosti stringa."
„Duboko kopiranje je drugo od plitkog kopiranja, kod dubokog kopiranja se takođe kloniraju polja referentnog tipa, kada se menja jedan objekat, drugi objekat ne prati promenu."
„Razumevši ovu tačku, pogledajmo primer."
class Book implements Cloneable{
private String bookName;
private int price;
// getter/setter i konstruktori su izostavljeni
@Override
public String toString() {
return super.toString().substring(26) +
" bookName='" + bookName + '\'' +
", price=" + price +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}Obratite pažnju, u ovom trenutku klasa Book je drugačija od plitkog kopiranja, prepisala je clone() metodu i implementira Cloneable interfejs. Svrha je da se pri dubokom kopiranju takođe može klonirati ovo polje.
class Writer implements Cloneable{
private int age;
private String name;
private Book book;
// getter/setter i konstruktori su izostavljeni
@Override
public String toString() {
return super.toString().substring(26) +
" age=" + age +
", name='" + name + '\'' +
", book=" + book +
')';
}
@Override
protected Object clone() throws CloneNotSupportedException {
Writer writer = (Writer) super.clone();
writer.setBook((Book) writer.getBook().clone());
return writer;
}
}Obratite pažnju, u ovom trenutku je klasa Writer takođe drugačija od prethodne, u clone() metodi, više ne poziva samo Object metodu clone() za kloniranje Writer, već takođe klonira Book.
Pogledaj testnu klasu.
class TestClone {
public static void main(String[] args) throws CloneNotSupportedException {
Writer writer1 = new Writer(18,"Erge");
Book book1 = new Book("Principi kompajlera",100);
writer1.setBook(book1);
Writer writer2 = (Writer) writer1.clone();
System.out.println("Posle dubokog kopiranja:");
System.out.println("writer1:" + writer1);
System.out.println("writer2:" + writer2);
Book book2 = writer2.getBook();
book2.setBookName("Trajni Tjuring");
book2.setPrice(70);
System.out.println("Posle promene writer2.book:");
System.out.println("writer1:" + writer1);
System.out.println("writer2:" + writer2);
}
}Ova testna klasa je potpuno ista kao prethodna testna klasa plitkog kopiranja, ali rezultat izvršavanja je različit.
Posle dubokog kopiranja:
writer1:Writer@6be46e8f age=18, name='Erge', book=Book@5056dfcb bookName='Principi kompajlera', price=100}}
writer2:Writer@6d00a15d age=18, name='Erge', book=Book@51efea79 bookName='Principi kompajlera', price=100}}
Posle promene writer2.book:
writer1:Writer@6be46e8f age=18, name='Erge', book=Book@5056dfcb bookName='Principi kompajlera', price=100}}
writer2:Writer@6d00a15d age=18, name='Erge', book=Book@51efea79 bookName='Trajni Tjuring', price=70}}Ne samo da su writer1 i writer2 različiti objekti, već su i book u njima različiti objekti. Stoga, promena book u writer2 neće uticati na writer1.

Međutim, duboko kopiranje realizovano kroz clone() metodu je prilično glomazno, zato što sva polja referentnog tipa moraju prepisati clone() metodu, kada je ugnježdenih objekata više, to je gotovo!
„Da li postoji dobar način?“ Sanmej je hitno pitala.
„Naravno, koristeći serijalizaciju.“ Sigurno sam odgovorio, „Serijalizacija je upisivanje objekta u tok za prenos, a deserijalizacija je čitanje objekta iz toka."
„Objekat upisan u tok je kopija originalnog objekta. Treba napomenuti da svaka klasa koja treba da bude serijalizovana mora implementirati Serializable interfejs, ovaj interfejs je sličan Cloneable interfejsu, oba su marker interfejsa."
Pogledaj primer.
class Book implements Serializable {
private String bookName;
private int price;
// getter/setter i konstruktori su izostavljeni
@Override
public String toString() {
return super.toString().substring(26) +
" bookName='" + bookName + '\'' +
", price=" + price +
'}';
}
}Book mora implementirati Serializable interfejs.
class Writer implements Serializable {
private int age;
private String name;
private Book book;
// getter/setter i konstruktori su izostavljeni
@Override
public String toString() {
return super.toString().substring(26) +
" age=" + age +
", name='" + name + '\'' +
", book=" + book +
')';
}
//duboko kopiranje
public Object deepClone() throws IOException, ClassNotFoundException {
// serijalizacija
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
// deserijalizacija
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
}Writer klasa takođe mora implementirati Serializable interfejs, i u ovoj klasi dodali smo metodu deepClone(), koristeći OutputStream za serijalizaciju, InputStream za deserijalizaciju, time realizuje duboko kopiranje.
Pogledaj primer.
class TestClone {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Writer writer1 = new Writer(18,"Erge");
Book book1 = new Book("Principi kompajlera",100);
writer1.setBook(book1);
Writer writer2 = (Writer) writer1.deepClone();
System.out.println("Posle dubokog kopiranja:");
System.out.println("writer1:" + writer1);
System.out.println("writer2:" + writer2);
Book book2 = writer2.getBook();
book2.setBookName("Trajni Tjuring");
book2.setPrice(70);
System.out.println("Posle promene writer2.book:");
System.out.println("writer1:" + writer1);
System.out.println("writer2:" + writer2);
}
}Razlika od prethodne testne klase je u tome što smo pozvali deepClone() metodu.
Posle dubokog kopiranja:
writer1:Writer@9629756 age=18, name='Erge', book=Book@735b5592 bookName='Principi kompajlera', price=100}}
writer2:Writer@544fe44c age=18, name='Erge', book=Book@31610302 bookName='Principi kompajlera', price=100}}
Posle promene writer2.book:
writer1:Writer@9629756 age=18, name='Erge', book=Book@735b5592 bookName='Principi kompajlera', price=100}}
writer2:Writer@544fe44c age=18, name='Erge', book=Book@31610302 bookName='Trajni Tjuring', price=70}}Rezultati testa su slični dubokom kopiranju realizovanom kroz clone() metodu.
„Međutim, Sanmej, treba obratiti pažnju, zato što serijalizacija uključuje čitanje i pisanje toka ulaza i izlaza, u perfomansu je mnogo gora od clone() metode koju implementira HotSpot virtuelna mašina." Rekao sam osećajno.
„Dobro, Erge, prvo idi da odmoriš, ostavi me da malo razmislim, sumiram razlike između plitkog i dubokog kopiranja."
„Mhm."
