Java Copy Constructor: Syntax and Deep vs Shallow Copy
java copy constructor: Learn how to implement a copy constructor in Java, control shallow and deep copying, and choose between copy constructors and clone().
A java copy constructor is a constructor that accepts an instance of the same class and uses its fields to initialize a new object. Unlike clone(), it does not rely on Object's native method or require implementing Cloneable. You write the copying logic yourself, which gives you full control over which fields are copied and how.
Declaring a Copy Constructor
The simplest copy constructor takes a single argument of the same class and copies each field. For immutable fields, you can delegate to the primary constructor to keep the logic in one place.
public class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } // Copy constructor public Point(Point other) { this(other.x, other.y); } }
This approach works well when all fields are immutable or when you only need a shallow copy. The new Point is completely independent because int values are primitives. For reference fields, the behavior depends on whether you copy the reference or create a new object for the field.
Shallow vs Deep Copy
A shallow copy copies the field values directly. If a field is a reference type, the new object points to the same underlying instance. A deep copy creates new instances for mutable reference fields so that changes to the copy do not affect the original.
Consider a class with a mutable List:
import java.util.ArrayList; import java.util.List; public class ShoppingCart { private List<String> items; public ShoppingCart(List<String> items) { this.items = new ArrayList<>(items); } // Shallow copy constructor public ShoppingCart(ShoppingCart other) { this.items = other.items; } // Deep copy constructor public ShoppingCart(ShoppingCart other, boolean deepCopy) { this.items = new ArrayList<>(other.items); } }
The shallow copy shares the same ArrayList instance. If you add an item through the copy, the original cart also changes. The deep copy creates a new list, so the two carts evolve independently. The choice depends on whether the original should remain isolated.
Copy Constructor vs clone()
The clone() method is often used for copying, but it has several drawbacks. It requires implementing Cloneable, returns an Object that needs casting, and its default implementation performs a shallow copy. Copy constructors are more explicit and type-safe.
| Aspect | Copy Constructor | clone() |
|---|---|---|
| Type safety | Returns the exact type | Returns Object, needs cast |
| Interface needed | None | Cloneable |
| Copy depth | Controlled by your code | Shallow by default |
| Final fields | Can be assigned in constructor | Cannot be assigned after creation |
| Error handling | Can throw checked exceptions | Throws CloneNotSupportedException |
For most use cases, a copy constructor is clearer and less error-prone. It also works with final fields, which clone() cannot handle because final fields must be assigned in the constructor.
Copy Constructors and Inheritance
When a class extends another, the copy constructor must copy the superclass fields as well. You can call the superclass copy constructor explicitly.
public class Vehicle { private String model; public Vehicle(String model) { this.model = model; } protected Vehicle(Vehicle other) { this(other.model); } } public class Car extends Vehicle { private int doors; public Car(String model, int doors) { super(model); this.doors = doors; } public Car(Car other) { super(other); this.doors = other.doors; } }
If the superclass does not expose a copy constructor, you can still copy its fields by calling a protected or public constructor that accepts the necessary values. This keeps the copying logic within the class hierarchy and avoids duplicating field access.
Common Mistakes and Edge Cases
One frequent mistake is forgetting to handle null arguments. If the copy constructor receives null, accessing other.field throws a NullPointerException. Decide whether null is a valid input and handle it explicitly.
Another issue arises with collections and maps. Copying a List with new ArrayList<>(other.list) creates a new list, but the elements themselves are still shared. If those elements are mutable, you need to copy each element to achieve a true deep copy.
Also, be careful with fields that are not directly accessible, such as those from a superclass or those wrapped in an immutable container. The copy constructor should reflect the actual state of the object, not just the fields you can access directly.
Performance and Maintainability Considerations
Copy constructors allocate a new object and copy each field. For small objects, this overhead is negligible. For large object graphs, a deep copy can be expensive because it may create many new instances. If performance is critical, consider whether a shallow copy or a different design, such as sharing immutable state, is more appropriate.
From a maintainability perspective, a copy constructor keeps copying logic in one place. If you add a new field, you update the copy constructor instead of hunting for every place that manually copies the object. This reduces the risk of forgetting to copy a new field.
When the object has many fields, you can use a builder or a static factory method that accepts the original object and returns a copy. The copy constructor remains the most direct and idiomatic approach in Java, and it integrates naturally with constructor chaining and validation.
One final consideration: if your class is meant to be subclassed, make the copy constructor protected so subclasses can call it. This preserves encapsulation while allowing extension.