Hiểu sâu về việc sao chép đối tượng trong Java - các game quay hũ uy tín

| Apr 23, 2025 min read

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ện Cloneable như sau:
package java.lang;
public interface Cloneable {}
  • Ghi đè phương thức clone() Ghi đè phương thức clone() được bảo vệ trong lớp Object và thay đổi phạm vi truy cập thành public. Theo quy ước, nên sử dụng super.clone() để gọi phương thức clone() từ lớp Object, qua đó thực hiện sao chép từng trường. Định nghĩa phương thức clone() trong lớp Object 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, sizerefrigerator.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à1nhà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à1nhà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) -

![](Hình ảnh liên quan) #Java