20/03/2024 Máy tính
Trong Java, việc sao chép đối tượng là quá trình tạo ra một bản sao của đối tượng hiện tại. Bản sao này sẽ có trạng thái và thuộc tính giống hệt như đối tượng gốc nhưng tồn tại độc lập trong bộ nhớ. Điều này có nghĩa là mọi thay đổi trên một đối tượng không ảnh hưởng đến đối tượng kia.
Để cho phép một lớp có thể được sao chép, cần đáp ứng các điều kiện sau:
- Thực hiện giao diện
Cloneable
Cloneable
là một giao diện đánh dấu, không chứa bất kỳ phương thức nào. Khi một lớp thực hiện giao diện này, nó thông báo rằng lớp đó có khả năng sao chép. Định nghĩa của giao diệnCloneable
như sau:
package java.lang;
public interface Cloneable {}
- Ghi đè phương thức
clone()
Ghi đè phương thứcclone()
được bảo vệ trong lớpObject
và thay đổi phạm vi truy cập thànhpublic
. Theo quy ước, nên sử dụngsuper.clone()
để gọi phương thứcclone()
từ lớpObject
, qua đó thực hiện sao chép từng trường. Định nghĩa phương thứcclone()
trong lớpObject
như sau:
package java.lang;
public class Object {
@IntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
}
Nếu không triển khai giao diện Cloneable
, khi gọi super.clone()
, ngoại lệ CloneNotSupportedException
sẽ được ném ra.
** _Lưu ý: Thiết kế sao chép đối tượng trong Java có một số “hạn chế”. Mặc dù một lớp cần triển khai giao diện Cloneable
để hỗ trợ sao chép, nhưng phương thức clone()
lại không được định nghĩa trong giao diện này. Do đó, ngay cả khi một lớp tuyên bố đã triển khai giao diện Cloneable
, không có cách nào bắt buộc nó phải chứa phương thức clone()
. _**
Dưới đây chúng ta sẽ thử nghiệm việc sử dụng sao chép đối tượng.
1 Thử nghiệm với phương thức clone()
Chúng ta sẽ tạo một lớp Nhà
(House) mới, bao gồm ba thuộc tính: tên (name
), kích thước (size
) và tủ lạnh (refrigerator
). Lớp này triển khai giao diện Cloneable
và ghi đè phương thức clone()
từ lớp Object
.
public class Nhà implements Cloneable {
private String name;
private Integer size;
private TủLạnh refrigerator;
public Nhà(String name, Integer size, TủLạnh refrigerator) {
this.name = name;
this.size = size;
this.refrigerator = refrigerator;
}
@Override
public Nhà clone() {
try {
return (Nhà) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
public static class TủLạnh {
private String name;
public TủLạnh(String name) {
this.name = name;
}
}
public static void main(String[] args) {
Nhà nhà1 = new Nhà("Nhà của Larry", 100, new TủLạnh("Tủ lạnh của Larry"));
Nhà nhà2 = nhà1.clone();
[Link đăng nhập Saigon777 Tặng 50k](/post/6293/) nhà2.name = "Nhà của Jacky";
nhà2.size = 99;
nhà2.refrigerator.name = "Tủ lạnh của Jacky";
System.out.println(nhà1); // Nhà@404b9385
System.out.println(nhà1.name); // Nhà của Larry
System.out.println(nhà1.size); // 100
System.out.println(nhà1.refrigerator); // Nhà$TủLạnh@6d311334
System.out.println(nhà1.refrigerator.name); // Tủ lạnh của Jacky
System.out.println(nhà2); // Nhà@682a0b20
System.out.println(nhà2.name); // Nhà của Jacky
System.out.println(nhà2.size); // 99
System.out.println(nhà2.refrigerator); // Nhà$TủLạnh@6d311334
System.out.println(nhà2.refrigerator.name); // Tủ lạnh của Jacky
}
}
Quan sát thấy rằng, khi lớp Nhà
ghi đè phương thức clone()
, nó trực tiếp gọi super.clone()
theo quy ước.
Khi kiểm tra trong phương thức main()
, ta phát hiện rằng bằng cách sử dụng nhà1.clone()
, ta đã nhận được bản sao nhà2
của đối tượng gốc nhà1
. In ra kết quả cho thấy giá trị hashCode
khác nhau, chứng minh rằng hai đối tượng này là những phiên bản riêng biệt, nhưng tất cả các giá trị thuộc tính đều giống nhau. Sau đó, khi gán lại giá trị cho name
, size
và refrigerator.name
của nhà2
, ta nhận thấy rằng hai trường đầu tiên không ảnh hưởng đến nhà1
, nhưng thay đổi ở refrigerator.name
lại ảnh hưởng đến nhà1
.
Tại sao điều này xảy ra?
1.1 Sao chép nông
Nguyên nhân là do việc gọi super.clone()
chỉ thực hiện sao chép nông. Nói cách khác, nó chỉ tạo ra một đối tượng mới và gán giá trị từng trường từ đối tượng gốc sang đối tượng sao chép. Nếu trường là kiểu nguyên thủy hoặc tham chiếu đến đối tượng bất biến, giá trị sẽ được truyền qua, đảm bảo rằng trường này hoàn toàn độc lập với trường gốc. Nhưng nếu trường là tham chiếu đến đối tượng có thể thay đổi, giá trị truyền đi thực chất vẫn là cùng một đối tượng.
Với ví dụ trên, cấu trúc bộ nhớ của nhà1
và nhà2
có thể được biểu diễn như sau:
!Sao chép nông
1.2 Sao chép sâu
Ta có thể thấy rằng việc gọi super.clone()
chỉ thực hiện sao chép nông. Nếu muốn sao chép cả đối tượng con mà trường tham chiếu đang trỏ tới, cần thêm xử lý phụ trợ.
Mã nguồn dưới đây cải tiến từ trước, nơi lớp TủLạnh
cũng triển khai giao diện Cloneable
và ghi đè phương thức clone()
. Ngoài ra, phương thức clone()
của lớp Nhà
cũng được sửa đổi để xử lý thêm (house.refrigerator = house.refrigerator.clone();
):
public class Nhà implements Cloneable {
private String name;
private Integer size;
private TủLạnh refrigerator;
public Nhà(String name, Integer size, TủLạnh refrigerator) {
this.name = name;
this.size = size;
this.refrigerator = refrigerator;
}
@Override
public Nhà clone() {
try {
Nhà nhà = (Nhà) super.clone();
nhà.refrigerator = nhà.refrigerator.clone();
return nhà;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
public static class TủLạnh implements Cloneable {
private String name;
public TủLạnh(String name) {
this.name = name;
}
@Override
public TủLạnh clone() {
try {
return (TủLạnh) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
Nhà nhà1 = new Nhà("Nhà của Larry", 100, new TủLạnh("Tủ lạnh của Larry"));
Nhà nhà2 = nhà1.clone();
nhà2.name = "Nhà của Jacky";
nhà2.size = 99;
nhà2.refrigerator.name = "Tủ lạnh của Jacky";
System.out.println(nhà1); // Nhà@404b9385
System.out.println(nhà1.name); // Nhà của Larry
System.out.println(nhà1.size); // 100
System.out.println(nhà1.refrigerator); // Nhà$TủLạnh@6d311334
System.out.println(nhà1.refrigerator.name); // Tủ lạnh của Larry
System.out.println(nhà2); // Nhà@682a0b20
System.out.println(nhà2.name); // Nhà của Jacky
System.out.println(nhà2.size); // 99
System.out.println(nhà2.refrigerator); // Nhà$TủLạnh@3d075dc0
System.out.println(nhà2.refrigerator.name); // Tủ lạnh của Jacky
}
}
Bây giờ, khi nhà2
thay đổi giá trị refrigerator.name
, nó không còn ảnh hưởng đến nhà1
nữa, tức là đã đạt được sao chép sâu.
Cấu trúc bộ nhớ của nhà1
và nhà2
lúc này có thể được biểu diễn như sau:
!Sao chép sâu
Tuy nhiên, nếu lớp TủLạnh
chứa thêm một lớp QuảTáo
, vấn đề cũ sẽ tái diễn. Mã nguồn trên chỉ sao chép đến mức TủLạnh
, nhưng đối với QuảTáo
, nó sẽ vẫn chia sẻ cùng một đối tượng. Điều này yêu cầu chúng ta lặp lại xử lý tương tự (triển khai Cloneable
cho lớp QuảTáo
và sửa đổi phương thức clone()
của lớp TủLạnh
).
Tóm lại, cách sao chép mặc định yêu cầu tuân thủ các quy tắc nhất định và khá phức tạp khi đối phó với các đối tượng lồng nhau.
2 Các cách thực hiện khác
Cách sao chép nguyên bản có vẻ rắc rối? Có cách nào khác để sao chép đối tượng không?
2.1 Sử dụng lớp công cụ khung
Lớp công cụ BeanUtils
từ khung Spring có thể giúp chúng ta sao chép từng trường giữa các đối tượng. Khi sử dụng lớp này, không cần triển khai giao diện Cloneable
hay ghi đè phương thức clone()
.
Dưới đây là phương thức mà BeanUtils
cung cấp để sao chép từ source
sang target
:
BeanUtils.copyProperties(Object source, Object target);
Để sử dụng, cần thêm phụ thuộc Maven sau:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>6.1.5</version>
</dependency>
Ví dụ sử dụng BeanUtils.copyProperties()
để sao chép đối tượng Nhà
:
import org.springframework.beans.BeanUtils;
public class NháCopyable {
private String name;
private Integer size;
private TủLạnh refrigerator;
public NháCopyable() {}
public NháCopyable(String name, Integer size, TủLạnh refrigerator) {
this.name = name;
this.size = size;
this.refrigerator = refrigerator;
}
public static class TủLạnh {
private String name;
public TủLạnh() {}
public TủLạnh(String name) {
this.name = name;
}
}
public static void main(String[] args) {
TủLạnh refrigerator = new TủLạnh("Tủ lạnh của Larry");
NháCopyable nhà1 = new NháCopyable("Nhà của Larry", 100, refrigerator);
NháCopyable nhà2 = new NháCopyable();
nhà2.refrigerator = new TủLạnh();
BeanUtils.copyProperties(nhà1, nhà2);
nhà2.name = "Nhà của Jacky";
nhà2.size = 99;
nhà2.refrigerator.name = "Tủ lạnh của Jacky";
System.out.println(nhà1); // NháCopyable@75828a0f
System.out.println(nhà1.name); // Nhà của Larry
System.out.println(nhà1.size); // 100
System.out.println(nhà1.refrigerator); // NháCopyable$TủLạnh@3abfe836
System.out.println(nhà1.refrigerator.name); // Tủ lạnh của Larry
System.out.println(nhà2); // NháCopyable@2ff5659e
System.out.println(nhà2.name); // Nhà của Jacky
System.out.println(nhà2.size); // 99
System.out.println(nhà2.refrigerator); // NháCopyable$TủLạnh@77afea7d
System.out.println(nhà2.refrigerator.name); // Tủ lạnh của Jacky
}
}
Rõ ràng, sử f8bet72 dụng BeanUtils.copyProperties()
mang lại kết quả mong đợi.
2.2 Sử dụng constructor sao chép
Một cách khác là cung cấp constructor sao chép hoặc phương thức nhà máy tĩnh để tự triển khai logic sao chép đối tượng.
public class Nhà {
private String name;
private Integer size;
private TủLạnh refrigerator;
public Nhà(Nhà nhà) { ... }
public static Nhà newInstance(Nhà nhà) { ... }
}
Nhà nhà2 = new Nhà(nhà1); // hoặc Nhà nhà2 = Nhà.newInstance(nhà1);
2.3 Sử dụng serialization và deserialization
Cách thứ ba là sử dụng serialization và deserialization để sao chép đối tượng. Cách này liên quan đến việc chuyển đổi đối tượng thành luồng byte và sau đó tái tạo lại từ luồng đó, tạo ra hai đối tượng hoàn toàn khác biệt. Tuy nhiên, để hỗ trợ serialization, lớp cần triển khai giao diện Serializable
. Hơn nữa, vì quá trình này nặng hơn, hiệu suất của nó không bằng phương thức clone()
nguyên bản.
Dưới đây là cách sử dụng lớp công cụ SerializationUtils
từ thư viện commons-lang3
để sao chép đối tượng:
SerializationUtils.clone(T object);
Phụ thuộc Maven như sau:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
Ví dụ sử dụng SerializationUtils.clone()
để sao chép đối tượng Nhà
:
import org.apache.commons.lang3.SerializationUtils;
import java.io.Serial;
import java.io.Serializable;
public class NhàSerializable implements Serializable {
@Serial
private static final long serialVersionUID = -3606554850313928707L;
private String name;
private Integer size;
private TủLạnh refrigerator;
public NhàSerializable(String name, Integer size, TủLạnh refrigerator) {
this.name = name;
this.size = size;
this.refrigerator = refrigerator;
}
public static class TủLạnh implements Serializable {
@Serial
private static final long serialVersionUID = 7744295794434285806L;
private String name;
public TủLạnh(String name) {
this.name = name;
}
}
public static void main(String[] args) {
TủLạnh refrigerator = new TủLạnh("Tủ lạnh của Larry");
NhàSerializable nhà1 = new NhàSerializable("Nhà của Larry", 100, refrigerator);
NhàSerializable nhà2 = SerializationUtils.clone(nhà1);
nhà2.name = "Nhà của Jacky";
nhà2.size = 99;
nhà2.refrigerator.name = "Tủ lạnh của Jacky";
System.out.println(nhà1); // NhàSerializable@5e9f23b4
System.out.println(nhà1.name); // Nhà của Larry
System.out.println(nhà1.size); // 100
System.out.println(nhà1.refrigerator); // NhàSerializable$TủLạnh@7e6cbb7a
System.out.println(nhà1.refrigerator.name); // Tủ lạnh của Larry
System.out.println(nhà2); // NhàSerializable@5b37e0d2
System.out.println(nhà2.name); // Nhà của Jacky
System.out.println(nhà2.size); // 99
System.out.println(nhà2.refrigerator); // NhàSerializable$TủLạnh@4459eb14
System.out.println(nhà2.refrigerator.name); // Tủ lạnh của Jacky
}
}
Rõ ràng, đối tượng được sao chép bởi SerializationUtils.clone()
là một đối tượng mới hoàn toàn khác biệt so với đối tượng gốc, đảm bảo rằng việc thay đổi không ảnh hưởng đến đối tượng gốc, đúng như mong muốn.
Tóm lại, bài viết đã giới thiệu kiến thức liên quan đến việc sao chép đối tượng trong Java, bao gồm khái niệm sao chép đối tượng, cách thực hiện, sự khác biệt giữa sao chép nông và sâu, cũng như các công cụ hữu ích giúp đơn giản hóa quá trình sao chép đối tượng. Tất cả mã nguồn minh họa đã được tải lên GitHub cá nhân, mời bạn đọc quan tâm hoặc Fork.
[1] Effective Java (Phiên bản thứ 3): Ghi đè clone thận trọng - [2] Wikipedia: clone (phương thức Java) - [3] Java Platform SE 8: Giao diện Cloneable - [4] Java Platform SE 8: Object.clone() - [5] CSDN Blog: Chi tiết về phương thức clone trong Java (Mẫu Prototype) - [6] SegmentFault: Shallow Copy và Deep Copy trong Java - [7] Hướng dẫn lập trình: Java Clone và Cloneable - [8] HowToDoInJava: Cloning trong Java, Deep và Shallow Copy, Copy Constructors - [9] DigitalOcean: Phương thức clone() trong Java - [10] CSDN Blog: Ba cách thực hiện sao chép đối tượng trong Java (Cloneable Interface, Serialization Java, FastJson Serialization) -
 #Java