+4

Các cách để sao chép một đối tượng trong Java

Trong Java, có một số cách khác nhau để sao chép một đối tượng. Mỗi phương pháp có những ưu và nhược điểm riêng, tùy thuộc vào bối cảnh sử dụng và yêu cầu về sâu độ của bản sao (shallow copy hoặc deep copy). Vậy sâu độ của bản sao là gì, trước khi đi vào nội dung chính của bài viết, chúng ta hãy cùng tìm hiểu qua hai khái niệm Shallow Copy & Deep Copy nhé.

A. Depth of copy (Sâu độ của bản sao)

"Sâu độ của bản sao" (Depth of copy) trong lập trình, đặc biệt trong ngôn ngữ như Java, thường đề cập đến mức độ mà dữ liệu của đối tượng được sao chép từ một đối tượng sang một đối tượng khác. Có hai loại chính: shallow copy (bản sao nông) và deep copy (bản sao sâu).

1. Shallow copy (Bản sao nông)

  • Trong một shallow copy, chỉ các trường ở cấp độ ngoài cùng của đối tượng được sao chép.

  • Nếu đối tượng có các trường tham chiếu đến các đối tượng khác, thì chỉ tham chiếu (địa chỉ bộ nhớ) của các đối tượng này được sao chép, chứ không phải đối tượng thực tế mà chúng tham chiếu đến.

  • Điều này có nghĩa là đối tượng gốc và bản sao có thể chia sẻ các đối tượng con tham chiếu, dẫn đến việc thay đổi trong một đối tượng có thể ảnh hưởng đến đối tượng kia.

Ví dụ về Shallow Copy

Giả sử chúng ta có một lớp Person và một lớp Address.

class Address {
    String city;
    String country;

    Address(String city, String country) {
        this.city = city;
        this.country = country;
    }
}

class Person implements Cloneable {
    String name;
    Address address;

    Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }

    // Override the clone method for shallow copy
    public Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }
}

Sử dụng phương thức clone() để tạo một shallow copy:

Address address = new Address("New York", "USA");
Person person1 = new Person("John", address);

try {
    Person person2 = person1.clone();
    person2.name = "Mike"; // Thay đổi tên không ảnh hưởng đến person1
    person2.address.city = "Los Angeles"; // Thay đổi thành phố sẽ ảnh hưởng đến cả person1

    System.out.println(person1.name); // In ra "John"
    System.out.println(person1.address.city); // In ra "Los Angeles"
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}

Trong ví dụ này, thay đổi tên của person2 không ảnh hưởng đến person1, nhưng việc thay đổi thành phố trong địa chỉ của person2 cũng thay đổi thành phố trong địa chỉ của person1, vì cả hai đối tượng chia sẻ cùng một thể hiện của Address.

2. Deep copy (Bản sao sâu)

  • Trong một deep copy, không chỉ các trường ở cấp độ ngoài cùng được sao chép, mà tất cả các đối tượng tham chiếu, cũng như các đối tượng mà chúng tham chiếu đến, đều được sao chép.
  • Kết quả là một bản sao hoàn toàn độc lập với đối tượng ban đầu. Thay đổi trong đối tượng sao chép không ảnh hưởng đến đối tượng gốc và ngược lại.
  • Deep copy thường phức tạp hơn và tốn kém hơn về mặt tài nguyên so với shallow copy.

Ví dụ về Deep Copy

Để thực hiện deep copy, chúng ta cần sửa đổi phương thức clone() để nó cũng sao chép các đối tượng tham chiếu.

class Person implements Cloneable {
    String name;
    Address address;

    Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }

    // Override the clone method for deep copy
    public Person clone() throws CloneNotSupportedException {
        Person clonedPerson = (Person) super.clone();
        clonedPerson.address = new Address(address.city, address.country);
        return clonedPerson;
    }
}

Sử dụng phương thức clone() để tạo một deep copy:

Address address = new Address("New York", "USA");
Person person1 = new Person("John", address);

try {
    Person person2 = person1.clone();
    person2.name = "Mike"; // Thay đổi tên không ảnh hưởng đến person1
    person2.address.city = "Los Angeles"; // Thay đổi thành phố không ảnh hưởng đến person1

    System.out.println(person1.name); // In ra "John"
    System.out.println(person1.address.city); // In ra "New York"
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}

Trong trường hợp này, thay đổi bất kỳ thuộc tính nào của person2 không ảnh hưởng đến person1, vì mỗi đối tượng có các thể hiện riêng của Address. Đây là một ví dụ của deep copy, nơi mà các đối tượng tham chiếu bên trong cũng được sao chép.

B. Các cách để sao chép một đối tượng

1. Sử dụng Phương thức clone()

  • Định nghĩa phương thức clone() trong lớp của đối tượng và thực hiện giao diện Cloneable.
  • Phương thức này tạo ra một bản sao nông của đối tượng, nghĩa là các tham chiếu đến các đối tượng khác trong đối tượng gốc sẽ không được sao chép.

Ví dụ tạo Shallow copy : Chính là ví dụ về Shallow copy của phần 1A ở trên

  • Còn nếu bạn muốn tạo Deep copy thì tương tự như trên, nhưng bạn cần tự thực hiện sao chép sâu cho tất cả các đối tượng tham chiếu bên trong.
  • Điều này đòi hỏi phải tự viết mã để sao chép từng trường đối tượng tham chiếu.

Ví dụ tạo Deep copy: Chính là ví dụ về Deep copy của phần 2A ở trên

2. Sử dụng Copy Constructor

  • Tạo một constructor mới nhận vào một đối tượng của cùng lớp và sao chép từng trường dữ liệu từ đối tượng nhập vào.
  • Phương pháp này cho phép bạn kiểm soát việc sao chép là sâu hay nông.

Ví dụ tạo shallow copy:

public class Person {
    private String name;
    private int age;

    public Person(Person other) {
        this.name = other.name;
        this.age = other.age;
    }

    // other constructors, getters, and setters
}
Person person1 = new Person("John", 25);
Person person2 = new Person(person1);

Ví dụ tạo deep copy:

Giả sử bạn có một lớp Person với một số trường dữ liệu và một trường tham chiếu đến một đối tượng khác, như Address.

public class Address {
    private String street;
    private String city;

    // Constructors, getters, setters

    // Copy Constructor cho Address
    // Sử dụng để tạo một bản sao sâu (deep copy) của Address
    public Address(Address other) {
        this.street = other.street;
        this.city = other.city;
    }
}

public class Person {
    private String name;
    private int age;
    private Address address;

    // Constructors, getters, setters

    // Copy Constructor cho Person
    // Sử dụng để tạo một bản sao sâu (deep copy) của Person
    public Person(Person other) {
        this.name = other.name;
        this.age = other.age;
        // Tạo một bản sao mới của Address để đảm bảo deep copy
        this.address = new Address(other.address);
    }
}

Ở đây, Person có một Copy Constructor, nó không chỉ sao chép các trường name và age, mà còn tạo một bản sao mới của đối tượng Address bằng cách sử dụng Copy Constructor của Address. Điều này đảm bảo rằng mỗi Person có một Address riêng biệt và thay đổi trong Address của một Person không ảnh hưởng đến Address của Person khác.

3. Sử dụng Java Serialization

Có các thư viện như Apache Commons Lang (với SerializationUtils.clone()) và các thư viện khác có thể cung cấp các chức năng sao chép sâu một cách dễ dàng và hiệu quả.

import java.io.*;

class MyClass implements Serializable {
    int number;
}

// Sử dụng
MyClass obj1 = new MyClass();
obj1.number = 5;

try {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream out = new ObjectOutputStream(bos);
    out.writeObject(obj1);
    out.flush();
    out.close();

    ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
    MyClass obj2 = (MyClass) in.readObject();
    // obj2 là deep copy của obj1
} catch (IOException | ClassNotFoundException e) {
    e.printStackTrace();
}

Java Serialization có thể được sử dụng để tạo ra deep copy, nhưng không phù hợp cho việc tạo shallow copy. Serialization hoạt động bằng cách chuyển đổi toàn bộ trạng thái của đối tượng (bao gồm cả các đối tượng tham chiếu bên trong) thành một dạng có thể lưu trữ hoặc truyền đi, và sau đó khôi phục lại trạng thái đó, tạo ra một bản sao độc lập hoàn toàn.

4. Sử dụng thư viện của bên thứ 3

Sử dụng thư viện bên thứ ba để tạo shallow copydeep copy có thể làm đơn giản hóa quy trình sao chép đối tượng trong Java. Một trong những thư viện phổ biến cho mục đích này là Apache Commons Lang, cung cấp các tiện ích dễ sử dụng cho việc sao chép đối tượng.

Shallow Copy với Apache Commons BeanUtils

  • Apache Commons BeanUtils có thể được sử dụng để tạo shallow copy của đối tượng. Nó sao chép các thuộc tính từ một đối tượng JavaBeans này sang một đối tượng JavaBeans khác.

  • Ví dụ Shallow Copy:

import org.apache.commons.beanutils.BeanUtils;

class MyClass {
    private String data;
    // Constructors, getters and setters
}

public class Main {
    public static void main(String[] args) {
        MyClass original = new MyClass();
        original.setData("Original data");

        MyClass shallowCopy = new MyClass();
        try {
            BeanUtils.copyProperties(shallowCopy, original);
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println(shallowCopy.getData()); // In ra "Original data"
    }
}
  • Trong ví dụ này, BeanUtils.copyProperties(shallowCopy, original) sao chép các thuộc tính từ original sang shallowCopy. Lưu ý rằng đây chỉ là một bản sao nông, vì vậy nếu các thuộc tính là các đối tượng tham chiếu, chúng vẫn sẽ tham chiếu đến cùng một đối tượng.

Deep Copy với Apache Commons Lang

  • Apache Commons Lang cung cấp SerializationUtils, mà bạn có thể sử dụng để tạo deep copy thông qua serialization. Điều này đòi hỏi đối tượng phải thực thi giao diện Serializable

  • Ví dụ Deep Copy:

import org.apache.commons.lang3.SerializationUtils;
import java.io.Serializable;

class MyClass implements Serializable {
    private String data;
    // Constructors, getters and setters
}

public class Main {
    public static void main(String[] args) {
        MyClass original = new MyClass();
        original.setData("Original data");

        MyClass deepCopy = SerializationUtils.clone(original);
        System.out.println(deepCopy.getData()); // In ra "Original data"
    }
}
  • Trong ví dụ này, SerializationUtils.clone(original) tạo một bản sao sâu của original. Bất kỳ thay đổi nào đối với deepCopy sẽ không ảnh hưởng đến original và ngược lại, bảo đảm rằng cả hai đối tượng là độc lập với nhau.

5. Sử dụng Reflection

  • Phương pháp này sử dụng API Reflection của Java để truy cập vào các trường của đối tượng và sao chép chúng.
  • Đây là một phương pháp nâng cao và có thể khá phức tạp.
import java.lang.reflect.Field;

class MyClass {
    int number;

    public MyClass deepCopy() throws IllegalAccessException, InstantiationException {
        MyClass copy = this.getClass().newInstance();
        for (Field field : this.getClass().getDeclaredFields()) {
            field.setAccessible(true);
            field.set(copy, field.get(this));
        }
        return copy;
    }
}

// Sử dụng
MyClass obj1 = new MyClass();
obj1.number = 5;

try {
    MyClass obj2 = obj1.deepCopy();
    // obj2 là deep copy của obj1
} catch (IllegalAccessException | InstantiationException e) {
    e.printStackTrace();
}

C. Lựa chọn tốt nhất để sao chép một đối tượng

Lựa chọn cách tốt nhất để sao chép một đối tượng trong Java phụ thuộc vào một số yếu tố cụ thể của tình huống bạn đang đối mặt. Dưới đây là một số khía cạnh cần xem xét để đưa ra quyết định:

1. Độ Phức Tạp của Đối Tượng:

  • Đối với các đối tượng đơn giản không chứa các tham chiếu sâu, sử dụng Shallow Copy qua phương thức clone() hoặc Copy Constructor có thể đủ.
  • Đối với các đối tượng phức tạp có các tham chiếu đến các đối tượng khác, Deep Copy là cần thiết. Trong trường hợp này, có thể sử dụng Serialization hoặc viết mã Deep Copy tùy chỉnh.

2. Hiệu Suất và Tài Nguyên:

  • Nếu hiệu suất là một vấn đề quan trọng (ví dụ, trong một ứng dụng có tần suất giao dịch cao), hãy tránh sử dụng Serialization do nó có thể khá chậm.
  • Copy Constructor và Shallow Copy thông qua clone() thường nhanh hơn nhưng phương thức mặc định thì không hỗ trợ deep copy mà ta cần ghi đè phương thức này và tự tay sao chép tất cả các đối tượng tham chiếu bên trong.

3. Khả Năng Bảo Trì và Mở Rộng:

  • Nếu dự án của bạn thường xuyên thêm hoặc bớt các trường dữ liệu, Copy Constructor và mã Deep Copy tùy chỉnh có thể đòi hỏi nhiều công sức bảo trì hơn.
  • BeanUtils hoặc Serialization có thể đơn giản hơn để bảo trì, nhưng chúng có những hạn chế riêng.

4. Sự Phụ Thuộc vào Thư Viện Bên Ngoài

  • Nếu bạn muốn tránh phụ thuộc vào thư viện bên ngoài, hãy tránh sử dụng BeanUtils hoặc các thư viện tương tự.
  • Reflection là một phương pháp mạnh mẽ nhưng có thể gây khó khăn trong bảo trì và cần kiến thức nâng cao về Java.

5. Bảo Mật và Nguyên Tắc Đóng Gói:

  • Phương pháp sử dụng Reflection có thể vi phạm các nguyên tắc đóng gói trong lập trình hướng đối tượng.
  • Nếu bạn cần một giải pháp đơn giản, nhanh chóng và không yêu cầu deep copy, Shallow Copy qua clone() hoặc Copy Constructor là lựa chọn tốt. Đối với các trường hợp cần sao chép sâu và độc lập hoàn toàn, việc viết mã Deep Copy tùy chỉnh hoặc sử dụng Serialization sẽ phù hợp hơn.

Cuối cùng, không có phương pháp "tốt nhất" chung cho mọi tình huống; lựa chọn phụ thuộc vào yêu cầu cụ thể của dự án và ưu tiên cá nhân của bạn về hiệu suất, bảo trì, và sự phức tạp.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí