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.
This story, “How to copy objects in Java: Shallow copy and deep copy” was originally published by
Copyright © 2024 IDG Communications, Inc.