rafael_del nero
Java Developer

Shallow and deep copy: Two ways to copy objects in Java

how-to
Jan 04, 20249 mins
JavaSoftware Development

Copying objects is a common Java programming operation that has one serious trap. Here's how to avoid copying from an object reference and only copy the instance and values you want.

Duplicate, copy, copying. Photograph of a man drawing himself
Credit: Shutterstock/pathdoc

Copying objects is a common operation in enterprise projects. When copying an object, we must ensure that we end up with a new instance that holds the values we want.

Domain objects are usually complex. Making a copy with the root object and composed objects is also not trivial.

Let’s explore the most effective ways to copy an object using shallow and deep copy techniques.

Object references

To correctly perform a shallow or deep object copy, we must first know what not to do. Understanding object references is essential for using shallow and deep copy techniques.

When making a copy of an object, it is important to avoid using the same object reference. It’s an easy mistake, as this example shows. To start, here’s the Product object we’ll use in our examples:


public class Product {

  private String name;
  private double price;

  public Product(String name, double price) {
    this.name = name;
    this.price = price;
  }

  public String getName() { return name; }
  public double getPrice() { return price; }

  public void setName(String name) { this.name = name; }
  public void setPrice(double price) { this.price = price; }

}

Now, let’s create and assign a Product object reference to another variable. It seems to be a copy, but in fact, it’s the same object:


public static void main(String[] args) {
    Product product = new Product("Macbook Pro", 3000);
    Product copyOfProduct = product;

    product.name = "Alienware";
    System.out.println(product.name);
    System.out.println(copyOfProduct.name);
  }

The output of this code is


Alienware
Alienware

Notice in the code above that we assign the object’s value to a different local variable, but this variable points to the same object reference. If we change the product or copyOfProduct objects, the result will be a change to the original Product object.

That’s because every time we create an object in Java, an object reference is created in Java’s memory heap. This lets us modify objects using their reference variables.

Shallow copy

The shallow copy technique allows us to copy simple object values to a new object without including the internal object values. As an example, here’s how to use the shallow copy technique to copy the Product object without using its object reference:


// Omitted the Product object

public class ShallowCopyPassingValues {
  public static void main(String[] args) {
    Product product = new Product("Macbook Pro", 3000);
    Product copyOfProduct = new Product(product.getName(), product.getPrice());

    product.setName("Alienware");
    System.out.println(product.getName());
    System.out.println(copyOfProduct.getName());
  }
}

The output is


Alienware
Macbook Pro

Notice in this code that when we pass the values from one object to the other, two different objects are created in the memory heap. When we change one of the values in the new object, the values will remain the same in the original object. This proves the objects are different and we’ve successfully executed the shallow copy.

Note: The Builder design pattern is another way to perform the same action.

Shallow copy with Cloneable

Since Java 7, we’ve had the Cloneable interface in Java. This interface provides another way to copy objects. Instead of implementing the copy logic manually, as we just did, we can implement the Cloneable interface and then implement the clone() method. Using Cloneable and the clone() method automatically results in a shallow copy.

I don’t like this technique because it throws a checked exception, and we have to manually cast a class type, which makes the code verbose. But using Cloneable might simplify the code if we have a huge domain object with many attributes.

Here’s what happens if we implement the Cloneable interface in a domain object and then override the clone() method:


public class Product implements Cloneable {

  // Omitted attributes, methods and constructor

  @Override
  protected Object clone() throws CloneNotSupportedException {
    return super.clone();
  }

}

Now, here’s the copy method in action again:


public class ShallowCopyWithCopyMethod {
  public static void main(String[] args) throws CloneNotSupportedException {
    Product product = new Product("Macbook Pro", 3000);
    Product copyOfProduct = (Product) product.clone();

    product.setName("Alienware");
    System.out.println(product.getName());
    System.out.println(copyOfProduct.getName());
  }
}

As you can see, the copy method works perfectly for making a shallow copy of an object. Using it means that we don’t need to copy every attribute manually.

Deep copy

The deep copy technique is the ability to copy a composed object’s values to another new object. If the Product object contains the Category object, for example, it’s expected that all the values from both objects would be copied to a new object.

What happens if the Product object has a composed object? Will the shallow copy technique work? Let’s see what happens if we try to use only the copy() method.

To start, we compose the Product class with the Order object:


public class Product implements Cloneable {
  
  // Omitted other attributes, constructor, getters and setters
  private Category category;

  public Category getCategory() { return category; }

}

Now, let’s do the same thing using the super.clone() method:


public class TryDeepCopyWithClone {

  public static void main(String[] args) throws CloneNotSupportedException {
    Category category = new Category("Laptop", "Portable computers");
    Product product = new Product("Macbook Pro", 3000, category);
    Product copyOfProduct = (Product) product.clone();

    Category copiedCategory = copyOfProduct.getCategory();

    System.out.println(copiedCategory.getName());
  }
}

The output is


Laptop

Notice that even though the output is “Laptop,” the deep copy operation did not happen. What happened instead is that we have the same Category object reference. Here’s the proof:


public class TryDeepCopyWithClone {
  public static void main(String[] args) throws CloneNotSupportedException {
    // Same code as the example above
    
    copiedCategory.setName("Phone");
    System.out.println(copiedCategory.getName());
    System.out.println(category.getName());
  }
}

Output:


Laptop
Phone
Phone

Notice in this code that a copy was not made when we changed the Category object. Instead, there was only an object assignment to a different variable. Therefore, we’ll change the object we created in the memory heap whenever we change the reference variable.

Deep copy with the clone() method

Now we know that the clone() method won’t work for a deep copy if we have a simple override. Let’s see how we can make it work.

First, we implement Cloneable in the Category class:


public class Category implements Cloneable {

  // Omitted attributes, constructor, getters and setters

  @Override
  protected Object clone() throws CloneNotSupportedException {
    return super.clone();
  }
}

Now, we have to change the implementation of the Product clone method also to clone the Category object:


public class ProductWithDeepCopy implements Cloneable {

  // Omitted attributes, constructor, getters and setters

  @Override
  protected Object clone() throws CloneNotSupportedException {
    this.category = (Category) category.clone();
    return super.clone();
  }
}

If we try to perform the deep copy with the same code example as above, we will get a real copy of the object values into a new object, as shown here:


public class TryDeepCopyWithClone {

  public static void main(String[] args) throws CloneNotSupportedException {
    Category category = new Category("Laptop", "Portable computers");
    Product product = new Product("Macbook Pro", 3000, category);
    Product copyOfProduct = (Product) product.clone();

    Category copiedCategory = copyOfProduct.getCategory();

    System.out.println(copiedCategory.getName());
    copiedCategory.setName("Phone");
    System.out.println(copiedCategory.getName());
    System.out.println(category.getName());
  }
}

The output is


Laptop
Phone
Laptop

Since we manually copied the category method in the copy() method of Product, it finally works. We will get a copy from Product and Category using the copy() method from Product.

This code proves that the deep copy worked. The values of the original and copied objects are different. Therefore, it’s not the same instance; it’s a copied object.

Shallow copy with serialization

it is sometimes necessary to serialize an object to transform it into bytes and pass it through a network. This operation can be dangerous because if not validated correctly, the serialized object might be exploited. The security of Java serialization is out of the scope of this article, but let’s see how it works with code.

We’ll use the same class from the example above but this time, we’ll implement the Serializable interface:


public class Product implements Serializable, Cloneable {
   // Omitted attributes, constructor, getters, setters and clone method
}

Notice that only the Product will be serialized since only the Product implements Serializable. The Category object won’t be serialized. Here’s an example:


public class ShallowCopySerializable {

  public static void main(String[] args) {
      try {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(bos);

        Product product = new Product("Macbook Pro", 3000);
        out.writeObject(product);
        out.flush();
        out.close();

        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream in = new ObjectInputStream(bis);
        Product clonedProduct = (Product) in.readObject();
        in.close();

        System.out.println(clonedProduct.getName());
        Category clonedCategory = clonedProduct.getCategory();
        System.out.println(clonedCategory);
      } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
      }
  }
}

The output is


Macbook Pro
null

Now, if we tried to populate the Category object into the Product object, the java.io.NotSerializableException would be thrown. That’s because the Category object does not implement Serializable.

Deep copy with serialization

Now,  let’s see what happens if we use the same code as above but add the following in the Category class:


public class Category implements Serializable, Cloneable {
  // Omitted attributes, constructor, getters, setters and clone method

  // Adding toString for a good Object description
  @Override
  public String toString() {
    return "Category{" + "name='" + name + ''' + ", description='" + description + ''' + '}';
  }
}

By running the same code as the shallow serializable copy code, we’ll get the result from Category, as well, and the output should be the following:


Macbook Pro
Category{name='Laptop', description='Portable computers'}

Note: To further explore Java serialization, try out the Java code challenge here.

Conclusion

Sometimes the shallow copy technique is all you need to clone an object superficially. But when you want to copy both the object and its internal objects, you must implement a deep copy manually. Here are the key takeaways from these important techniques.

What to remember about shallow copy

  • A shallow copy creates a new object but shares the references of the internal objects with the original object.
  • The copied and original objects refer to the same objects in memory.
  • Changes made to the internal objects through one reference will be reflected in both the copied and original objects.
  • Shallow copy is a simple and efficient process.
  • Java provides a default implementation of shallow copy through the clone() method.

What to remember about deep copy

  • A deep copy creates a new object and also creates new copies of its internal objects.
  • The copied and original objects have independent copies of the internal objects.
  • Changes made to the internal objects through one reference will not affect the other.
  • Deep copy is a more complex process, especially when dealing with object graphs or nested references.
  • Deep copy must be implemented explicitly, either manually or using libraries or frameworks.