#1 Khái niệm Copy

Để bắt đầu, có lẽ chúng ta cần ôn lại khái niệm Copy trong Java. Cụ thể ở đây, ta cần phân biệt giữa Reference Copy (Copy tham chiếu) và Object Copy (Copy toàn bộ object).

Giả sử chúng ta có Car object và biến myCar1 tham chiếu tới object này. Nếu tạo một Reference Copy tới Car Object, ta sẽ có biến myCar2, nhưng cả myCar1 và myCar2 đều trỏ chung đến 1 object là Car object.
Nếu chúng ta copy chính Car object và khai báo một biến myCar2 tham chiếu tới object vừa được copy thì chúng ta có 2 biến tham chiếu tới 2 object riêng biệt:

Và trong bài viết này, chúng ta chỉ bàn đến Object Copy.

#2 Khái niệm object

Ở đây chúng ta có thể xem một Object như là một sản phẩm cấu thành từ nhiều Object khác. Hãy xem sơ đồ sau để có cái nhìn trực quan:

Một Object kiểu Person sẽ có 2 thuộc tính là Name và Address. Tuy nhiên 2 thuộc tính này cũng là các Object. Name và Address sẽ tiếp tục chứa các thuộc tính tương ứng (và trong số các thuộc tính này có thể là các Object...).

Tại sao chúng ta lại cần copy các Object? Nhu cầu này xuất hiện khi chúng ta muốn thay đổi hoặc di chuyển chúng mà vẫn phải đảm bảo được tính nguyên bản của các Object này. Có nhiều cách Copy object đã được nhắc đến trong bài viết này của tác giả. Tuy nhiên, hôm nay chúng ta sẽ đi sâu vào phương pháp sử dụng copy constructor.

Tham khảo các khóa học lập trình online, onlab, và thực tập lập trình tại TechMaster

#3 Shallow Copy

Shallow Copy sẽ chỉ copy Object "chính" mà không copy các Object "con". Tức là 2 bản sao Object "chính" sẽ cùng chứa tham chiếu đến các object con. Để dễ hình dung, bạn có thể nhìn vào sơ đồ Object Person bên trên. Với Shallow Copy, sẽ có thêm một object Person mới được tạo ra, tuy nhiên 2 Object "con" là Name và Address thì vẫn thế. Khi đó, object Person mới và object Person "cũ" sẽ cùng tham chiếu tới 2 object "con" này.

public class Person {

    private Name name;

    private Address address;

    public Person(Person originalPerson) {

         this.name = originalPerson.name;

         this.address = originalPerson.address;

    }

[…]

}

Hạn chế của Shallow Copy nằm ở chỗ, 2 object "cũ" và "mới"  không hề hoạt động độc lập, ràng buộc giữa chúng chính là các Object "con" mà cả 2 cùng tham chiếu đến. Do đó, khi một object "cũ" hoặc "mới" thay đổi giá trị của các Object "con" này, kết quả sẽ ảnh hưởng tới object còn lại.

Chúng ta có thể thấy ngay được hạn chế này qua đoạn code sau:

Person mother = new Person(new Name(…), new Address(…));

[…]

Person son  = new Person(mother);

[…]

son.moveOut(new Street(…), new City(…));

Ở đây, chúng ta có 2 mẹ con - mother và son. Object son được tạo ra bằng việc shallow copy Object mother  Khi con còn bé thì 2 mẹ con sống chung với nhau, đồng nghĩa với địa chỉ nhà của mẹ và con là một. Tuy nhiên, khi con đã lớn và mua nhà riêng, địa chỉ của Object son sẽ phải thay đổi. Tuy nhiên nếu thay đổi trong trường hợp này thì địa chỉ của Object mother cũng thay đổi theo. Vì sao? Vì cả 2 object son và mother cùng tham chiếu tới một object Address.

Khi đó, sơ đồ của 2 object:

#4 Deep Copy

Khác với Shallow Copy, Deep copy sẽ copy ra một object độc lập hoàn toàn với object cũ và vẫn giữ được tính nguyên bản của nó.

Thể hiện Deep Copy trong code:

public class Person {

    private Name name;

    private Address address;

    public Person(Person otherPerson) {

         this.name    =  new Name(otherPerson.name);

         this.address =  new Address(otherPerson.address);

    }

[…]

}

Với Deep Copy, chúng ta có thể thử lại ví dụ của 2 mẹ con để thấy được kết quả.

Tuy nhiên, mọi thứ chưa dừng lại ở đây. Để thực hiện Deep Copy hoàn chỉnh, chúng ta phải tiếp tục copy tất cả các Object "con" có trong Object cần copy cho đến khi chỉ còn lại các biến Primitive Types hoặc Immutable Objects.

public class Street {

    private String name;

    private int number;

    public Street(Street otherStreet){

         this.name = otherStreet.name;

         this.number = otherStreet.number;

    }

[…]

}

Từ phần đầu bì viết, ta thấy Object Street gồm 2 instance variable là name (kiểu String) và number (kiểu int).

number thuộc primitive type, do đó không được xem là object. Khi tạo một instance variable thứ hai, chúng ta đã tự động tạo một bản copy độc lập của number. Trong khi đó, String là Immutable Object, với Immutable Object, ta không cần thực hiện Deep Copy cho chúng.

Tổng kết

Hãy lưu ý rằng Deep Copy cho phép chúng ta thay đổi các chi tiết bên trong một Object. Tuy nhiên nếu cứ làm như vậy thì chất lượng code của chúng ta sẽ ngày một đi xuống (Hãy tham khảo Open-Closed principle).

Trong lập trình hướng đối tượng, điều này đã vi phạm nguyên tắc encapsulation.

Thay vì thay đổi trực tiếp, chúng ta nên khởi tạo Object mới rồi gán giá trị thuộc tính ứng với Object con.

Person mother = new Person(new Name(…), new Address(…));

[…]

Person son  = new Person(mother);

[…]

son.moveOut(new Address(...));

Techmaster via Dzone