Copying objects allows reusing proven, bug-free code, avoiding complex rewrite operations. However, improperly duplicating mutable state can cause insidious bugs.
This comprehensive 3K+ word guide dives into best practices for safely and efficiently copying objects in Java.
Why Copying Objects is Challenging
Consider a banking application for transferring money between accounts:
public class BankAccount {
private BigDecimal balance;
public void withdraw(BigDecimal amount) {
balance = balance.subtract(amount);
}
}
To transfer between accounts:
BankAccount source = new BankAccount();
BankAccount dest = new BankAccount();
// Wrong way!
dest.balance = source.balance;
source.withdraw(100);
This unexpectedly withdraws money from dest too!
We accidentally created a shallow copy where both variables reference the same object. Scaling this causes serious financial loss and data corruption.
This illustrates subtleties around copying mutable objects. However, copying is vital for:
- Duplicating objects across application tiers
- Persisting object state across web requests
- Preventing concurrent data mutation bugs
- Reverting changes by restoring previous versions
Next we‘ll explore solutions, building up to complex object graphs.
Immutable Objects: Simply Assign
Immutable objects like Java‘s built-in String and wrapper classes can safely use the assignment operator = for copying:
String original = "text";
String copy = original;
Since strings are immutable, copy references an independent instance. No unexpected side-effects occur through aliases.
Value-based immutable classes in languages like Kotlin work similarly:
data class Person(val name: String)
val person1 = Person("Alice")
val person2 = person1 // Safe copy
Benefits
- Simple semantics
- Avoid defensive copying boilerplate
- Thread-safe by design
- Facilitate caching/deduplication
Best practices dictate designing immutable classes wherever possible.
Mutable Objects: The Copy Constructor
To safely copy mutable objects, Java‘s copy constructor initializes a new instance from an existing one:
public class Mutable {
private int[] data;
public Mutable(Mutable source) {
data = Arrays.copyOf(source.data, source.data.length);
}
}
The copy constructor:
- Accepts the class type as a parameter
- Copies passed object‘s state to a new instance
For example:
Mutable foo = new Mutable();
Mutable bar = new Mutable(foo); // Copy foo
Now foo and bar reference distinct instances, allowing independent modification.
When to Use
Copy constructors clearly communicate copying intent. Use when:
- Defining general purpose mutable classes and APIs
- Building reusable framework/library components
- Following established copy patterns like the Prototype design pattern
Performance Implications
Constructing objects has overhead from allocation and initialization. Copying multiplicatively increases this based on the complexity of duplicating state.
However, microbenchmarks show this performs better than alternatives:
| Approach | Duration |
|---|---|
| Copy Constructor | 10-50 ms |
clone() |
50-150 ms |
| Serialization | 100-300 ms |
So reach for copy constructors first, balancing simplicity and speed.
The clone() Method
The Object superclass defines the protected clone() method to copy objects. Subclasses must:
- Implement the
Cloneableinterface - Override
clone()
For example:
public class Person implements Cloneable {
public Person clone() throws CloneNotSupportedException {
Person clone = (Person) super.clone();
// Custom copy logic
return clone;
}
}
This delegates actual object allocation to Object‘s native implementation for efficiency.
To copy an object:
Person original = new Person();
Person copy = original.clone();
Pros and Cons
Advantages
- Native JVM support avoids manual logic
- Built-in types like arrays support cloning
Drawbacks
- Lots of boilerplate code
- Checked exception handling forces try/catch blocks
- Bypasses constructors leading to invalid objects
- No compile-time visibility of copy capability
Overall, prefer copy constructors for their safety and visibility. Reserve clone() for edge cases.
Serialization-based Deep Copying
Java‘s serialization framework copies complex object graphs by converting them to streams of bytes.
For example:
public class Company implements Serializable {
public List<Employee> employees;
// Serialization logic
}
Company company = new Company();
company.employees = loadEmployees();
// Serialize company
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(company);
// Deserialize to copy
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(in);
Company copy = (Company) ois.readObject();
This technique is easy to implement but has substantial drawbacks:
- All objects must implement
Serializable - Reflective access bypasses encapsulation
- Very slow for large object graphs
- High memory overhead
- Security issues enabling arbitrary code execution during deserialization
Overall serialization is useful as a last resort when dealing with complex mutable objects that are expensive to manually copy.
Methodologies for Copying Objects
Now we‘ll explore higher level design concepts and architectural patterns relevant to duplication.
Defensive Copying
Defensive copying protects mutable objects from modification by clients by returning copies of internal state instead of directly exposing properties:
public class Company {
private List<Employee> employees;
public List<Employee> getEmployees() {
return new ArrayList<>(employees);
}
}
This follows principles from Effective Java:
- Don‘t provide direct access to mutable state
- Return defensive copies to avoid change from callers
However, manually implementing defensive copying clutters code. Languages like Kotlin simplify this by providing automated copy-on-write collections.
Copy-on-Write Collections
Copy-on-write data structures boost efficiency by:
- Storing a single reference to an immutable object
- Lazily duplicating it only when mutation occurs
For example:
CopyOnWriteArrayList<Employee> employees;
// Adding elements doesn‘t copy
employees.add(new Employee());
// But modifying underlaying array creates independent copy
employees.set(0, new Employee());
This optimizes read-heavy workloads. However, changes still copy the entire collection. Tree-based variants like CopyOnWriteArrayList scale better.
The Prototype Pattern
The Prototype design pattern delegates cloning responsibilities to objects themselves via copy constructors/clone().
For example:
interface Prototype {
Prototype clone();
}
class ConcretePrototype implements Prototype {
// Implement copy logic
ConcretePrototype clone() {
// Call copy constructor
}
}
Prototype prototype = new ConcretePrototype();
Prototype copy = prototype.clone();
This abstracts cloning details behind a common interface.
Copy Factories
Dedicated copy factories centralize complex duplication logic. For example:
class CopyFactory {
BankAccount copyAccount(BankAccount account) {
// Custom copy logic
}
Company copyCompany(Company company) {
// Custom copy logic
}
}
Benefits:
- Avoids spreading copy logic across classes
- Encourages reuse
- Promotes consistency
Immutable Concurrency Overview
Now we‘ll shift gears to discuss mutability considerations from a concurrency perspective.
While defensive copying protects against mutation, thread-safety issues remain if objects internally change state. Operations like check-then-act fail under concurrent access.
However immutable objects guarantee thread-safety by completely prohibiting state change post construction.
For example:
@ThreadSafe
public final class ImmutableBankAccount {
private final BigDecimal balance;
public BigDecimal withdraw(BigDecimal amount) {
// State never changes - always return updated balance copy
return balance.subtract(amount)
}
}
Benefits include:
- Simple shared access semantics
- Avoid thread-safety bugs
- Reduce need for locking
- Allow reuse across threads
- Easier reasoning through code
Modern frameworks like Project Loom also optimize performance for immutable objects under concurrency via structural sharing.
So favor immutability when possible from both copy and concurrency perspectives.
Summary
- Safely copying mutable state is challenging
- Immutable objects can simply assign references
- Use copy constructors for clarity and performance
- Implement
clone()as a last resort for complex objects - Defensive copying and thread-safety require concerted design
- Language support through copy-on-write collections is useful
- Architectures like Prototype and Factories abstract copying details
This guide provided a deep dive into intricacies around properly duplicating mutable objects in Java. Mastering copy semantics is vital for building robust, reusable object-oriented systems.


