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:

  1. Accepts the class type as a parameter
  2. 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:

  1. Implement the Cloneable interface
  2. 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:

  1. Storing a single reference to an immutable object
  2. 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.

Similar Posts