Deep Copy vs Shallow Copy in Java

1. Introduction

When dealing with objects in Java, we often encounter scenarios where we need to duplicate an object. There are two main techniques to achieve this duplication: shallow copy and deep copy.

Each method has its own advantages, drawbacks, and use cases, which we’ll explore in detail and concise unit tests to highlight their differences.

2. Model

Let’s consider a scenario where we have a Customer class with a nested Account class. We’ll demonstrate both shallow copy and deep copy using these classes:

public class Account {
    private String accountNumber;
    private double balance;
    // standard constructors, getters and setters
}
public class Customer {
    private String name;
    private int age;
    private Account account;
    // standard constructors, getters and setters
}

3. Shallow Copy

Shallow copy creates a new object but copies only the references to the data contained in the original object. This means that the copied object shares references with the original object, pointing to the same underlying data.

As a result, changes made to the copied object’s fields directly affect the original object, and vice versa:

@Test
public void when_modifying_original_customer_then_shallow_copy_should_change() {
    Account originalAccount = new Account("112233_test", 1000.1);
    Customer originalCustomer = new Customer("DevStaq", 30, originalAccount);
    Customer shallowCopyCustomer = new Customer(originalCustomer.getName(), originalCustomer.getAge(), originalCustomer.getAccount());
    originalAccount.setBalance(732.5);
    assertEquals(originalCustomer.getAccount(), shallowCopyCustomer.getAccount());
}

In above example, the shallow copy, both the original and copied Customer share the same Account object. So, modifying the Account in one reflects in the other.

4. Deep Copy

In contrast, a thorough copy creates a completely new object with its own copy of all the data contained in the original object. This includes not only the object itself but also any objects it references, recursively copying all nested objects.

In other words, a thorough copy creates a complete twin, ensuring that changes made to the copied object do not affect the original and vice versa.

4.1. Copy Constructor

In Java, a copy constructor is a special constructor used to create a new Object by copying the state of an existing Object.

Let’s consider our example model above, and we want to implement a copy constructor for deep copying:

public Account(Account account) {
    this(account.getAccountNumber(), account.getBalance());
}
public Customer(Customer customer) {
    this(customer.getName(), customer.getAge(), new Account(customer.getAccount()));
}

Therefore, accidental modifications are prevented. Let’s test this to confirm its effectiveness:

@Test
public void when_modifying_original_customer_then_deep_copy_should_not_change() {
    Account originalAccount = new Account("112233_test", 1000.1);
    Customer originalCustomer = new Customer("DevStaq", 30, originalAccount);
    Customer deepCopyCustomer = new Customer(originalCustomer);
    originalAccount.setBalance(800.0);
    assertNotEquals(originalCustomer.getAccount(), deepCopyCustomer.getAccount());
}

In the above example, we’ve implemented copy constructors for both the Customer and Account classes.

When we perform a deep copy of the Object, we create an entirely new Object graph, where each object and its dependencies are duplicated. This ensures that the copied object is independent of the original, making it suitable for scenarios where we need to modify the copied object without affecting the original state.

4.2. Cloneable Interface

The Cloneable interface in Java provides a way to indicate that an object can be cloned, allowing for the creation of shallow copies.

To enable deep copying using the Cloneable interface, we need to override the clone() method in our class and ensure that all nested objects are also cloned deeply.

Again, let’s consider our example Account model above and add clone() method:

@Override
public Account clone() {
    try {
        return (Account) super.clone();
    } catch (CloneNotSupportedException e) {
        return new Account(this.getAccountNumber(), this.getBalance());
    }
}

Now let’s implement clone() for Customer class:

@Override
public Customer clone() {
    Customer customer = null;
    try {
        customer = (Customer) super.clone();
    } catch (CloneNotSupportedException e) {
        customer = new Customer(this.getName(), this.getAge(), this.getAccount());
    }
    customer.account = (Account) this.account.clone();
    return customer;
}

Now, let’s validate this with a test case:

@Test
public void when_modifying_original_customer_then_deep_copy_should_not_change_when_using_cloneable_interface() {
    Account originalAccount = new Account("112233_test", 1000.1);
    Customer originalCustomer = new Customer("DevStaq", 30, originalAccount);
    Customer deepCopyCustomer = (Customer) originalCustomer.clone();
    originalAccount.setBalance(800.0);
    assertNotEquals(originalCustomer.getAccount(), deepCopyCustomer.getAccount());
}

In the above example, we’ve implemented the Cloneable interface in both the Customer and Account classes. We’ve overridden the clone() method in each class to ensure that nested objects are also cloned deeply, thus achieving deep copying.

5. Additional Information

While implementing custom solutions for deep copying objects in Java is feasible in many cases, there are scenarios where it’s impractical or even impossible to modify existing classes to add constructors or override the clone() method.

In such situations, leveraging external libraries can provide a convenient and efficient solution for copying complex object graphs without the need for extensive manual coding.
Some popular libraries:

6. Conclusion

In conclusion, understanding the differences between deep and shallow copying is essential for writing robust and efficient Java code. By choosing the right approach based on our application’s requirements, we can ensure that our code behaves as expected and performs optimally.

Scroll to Top