Copy Constructors in Java

Introduction

Imagine you have a important document that you need to share with someone, but you don't want to give away your original. So you make a photocopy—it has all the same content as the original, but it's a completely separate physical copy. In Java, copy constructors are like photocopy machines for objects—they create a new object that's an exact copy of an existing object, but completely independent.

Copy constructors are special constructors that create a new object by copying all the fields from an existing object of the same class. They're essential for creating duplicate objects without affecting the original!


What are Copy Constructors?

A copy constructor is a constructor that takes an object of the same class as a parameter and creates a new object that's a copy of the passed object. It initializes the new object with the same values as the existing object.

Key Characteristics:

  • Takes same-class object as parameter
  • Creates deep or shallow copy depending on implementation
  • Provides controlled object copying
  • Alternative to Object.clone()
  • More readable and maintainable than cloning

Code Explanation with Examples

Example 1: Basic Copy Constructor

class Person {
private String name;
private int age;
private String email;
// Regular constructor
public Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
// ✅ COPY CONSTRUCTOR - takes Person object as parameter
public Person(Person original) {
// Copy all fields from original object
this.name = original.name;
this.age = original.age;
this.email = original.email;
System.out.println("Copy created for: " + original.name);
}
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public void displayInfo() {
System.out.println("Name: " + name);
System.out.println("Age: " + age);
System.out.println("Email: " + email);
System.out.println("---------------");
}
}
public class BasicCopyConstructorDemo {
public static void main(String[] args) {
// Create original person
Person original = new Person("Alice Johnson", 25, "[email protected]");
System.out.println("=== ORIGINAL PERSON ===");
original.displayInfo();
// Create copy using copy constructor
Person copy = new Person(original);
System.out.println("=== COPIED PERSON ===");
copy.displayInfo();
// Modify the copy - original remains unchanged
System.out.println("=== MODIFYING COPY ===");
copy.setName("Bob Smith");
copy.setAge(30);
copy.setEmail("[email protected]");
System.out.println("Original after modification:");
original.displayInfo();
System.out.println("Copy after modification:");
copy.displayInfo();
}
}

Output:

=== ORIGINAL PERSON ===
Name: Alice Johnson
Age: 25
Email: [email protected]
---------------
Copy created for: Alice Johnson
=== COPIED PERSON ===
Name: Alice Johnson
Age: 25
Email: [email protected]
---------------
=== MODIFYING COPY ===
Original after modification:
Name: Alice Johnson
Age: 25
Email: [email protected]
---------------
Copy after modification:
Name: Bob Smith
Age: 30
Email: [email protected]
---------------

Example 2: Shallow Copy vs Deep Copy

import java.util.Arrays;
class Course {
private String courseName;
private String instructor;
public Course(String courseName, String instructor) {
this.courseName = courseName;
this.instructor = instructor;
}
// Copy constructor for Course
public Course(Course original) {
this.courseName = original.courseName;
this.instructor = original.instructor;
}
// Getters and setters
public String getCourseName() { return courseName; }
public void setCourseName(String courseName) { this.courseName = courseName; }
public String getInstructor() { return instructor; }
public void setInstructor(String instructor) { this.instructor = instructor; }
@Override
public String toString() {
return courseName + " by " + instructor;
}
}
class Student {
private String name;
private int[] grades;  // Mutable array
private Course course; // Mutable object reference
// Regular constructor
public Student(String name, int[] grades, Course course) {
this.name = name;
this.grades = grades;  // ❌ SHALLOW COPY - shares reference
this.course = course;  // ❌ SHALLOW COPY - shares reference
}
// ✅ DEEP COPY CONSTRUCTOR
public Student(Student original) {
this.name = original.name;
// ✅ DEEP COPY for array - create new array and copy elements
if (original.grades != null) {
this.grades = new int[original.grades.length];
for (int i = 0; i < original.grades.length; i++) {
this.grades[i] = original.grades[i];
}
} else {
this.grades = null;
}
// ✅ DEEP COPY for Course object - create new Course using its copy constructor
if (original.course != null) {
this.course = new Course(original.course);
} else {
this.course = null;
}
System.out.println("Deep copy created for: " + original.name);
}
public void displayInfo() {
System.out.println("Name: " + name);
System.out.println("Grades: " + Arrays.toString(grades));
System.out.println("Course: " + course);
System.out.println("---------------");
}
// Method to demonstrate the problem with shallow copy
public void modifyGrades() {
if (grades != null && grades.length > 0) {
grades[0] = 100; // Modify first grade
}
}
public void modifyCourse() {
if (course != null) {
course.setInstructor("Dr. Changed");
}
}
}
public class ShallowVsDeepCopyDemo {
public static void main(String[] args) {
// Create original objects
int[] grades = {85, 92, 78};
Course course = new Course("Java Programming", "Dr. Smith");
Student original = new Student("John Doe", grades, course);
System.out.println("=== ORIGINAL STUDENT ===");
original.displayInfo();
// Create deep copy
Student deepCopy = new Student(original);
System.out.println("=== DEEP COPY (BEFORE MODIFICATION) ===");
deepCopy.displayInfo();
// Modify the deep copy
System.out.println("=== MODIFYING DEEP COPY ===");
deepCopy.modifyGrades();
deepCopy.modifyCourse();
System.out.println("Original after deep copy modification:");
original.displayInfo();
System.out.println("Deep copy after modification:");
deepCopy.displayInfo();
// Demonstrate shallow copy problem
System.out.println("=== SHALLOW COPY PROBLEM DEMO ===");
Student shallowCopy = new Student("Problem Student", grades, course);
System.out.println("Before modification:");
original.displayInfo();
shallowCopy.displayInfo();
// Modify through shallow copy - affects original!
shallowCopy.modifyGrades();
shallowCopy.modifyCourse();
System.out.println("After modification through shallow copy:");
original.displayInfo();  // ❌ Original is affected!
shallowCopy.displayInfo();
}
}

Output:

=== ORIGINAL STUDENT ===
Name: John Doe
Grades: [85, 92, 78]
Course: Java Programming by Dr. Smith
---------------
Deep copy created for: John Doe
=== DEEP COPY (BEFORE MODIFICATION) ===
Name: John Doe
Grades: [85, 92, 78]
Course: Java Programming by Dr. Smith
---------------
=== MODIFYING DEEP COPY ===
Original after deep copy modification:
Name: John Doe
Grades: [85, 92, 78]
Course: Java Programming by Dr. Smith
---------------
Deep copy after modification:
Name: John Doe
Grades: [100, 92, 78]
Course: Java Programming by Dr. Changed
---------------
=== SHALLOW COPY PROBLEM DEMO ===
Before modification:
Name: John Doe
Grades: [85, 92, 78]
Course: Java Programming by Dr. Smith
---------------
Name: Problem Student
Grades: [85, 92, 78]
Course: Java Programming by Dr. Smith
---------------
After modification through shallow copy:
Name: John Doe
Grades: [100, 92, 78]
Course: Java Programming by Dr. Changed
---------------
Name: Problem Student
Grades: [100, 92, 78]
Course: Java Programming by Dr. Changed
---------------

Example 3: Real-World Bank Account System

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
class Transaction {
private Date timestamp;
private String type;
private double amount;
public Transaction(String type, double amount) {
this.timestamp = new Date();
this.type = type;
this.amount = amount;
}
// Copy constructor for Transaction
public Transaction(Transaction original) {
this.timestamp = new Date(original.timestamp.getTime()); // Deep copy Date
this.type = original.type;
this.amount = original.amount;
}
// Getters
public Date getTimestamp() { return timestamp; }
public String getType() { return type; }
public double getAmount() { return amount; }
@Override
public String toString() {
return String.format("%s: $%.2f at %s", type, amount, timestamp);
}
}
class BankAccount {
private String accountNumber;
private String accountHolder;
private double balance;
private List<Transaction> transactionHistory;
// Regular constructor
public BankAccount(String accountNumber, String accountHolder, double initialBalance) {
this.accountNumber = accountNumber;
this.accountHolder = accountHolder;
this.balance = initialBalance;
this.transactionHistory = new ArrayList<>();
this.transactionHistory.add(new Transaction("OPENING", initialBalance));
System.out.println("Account created: " + accountNumber);
}
// ✅ COPY CONSTRUCTOR - creates a snapshot of account
public BankAccount(BankAccount original) {
this.accountNumber = "COPY-" + original.accountNumber;
this.accountHolder = original.accountHolder;
this.balance = original.balance;
// ✅ DEEP COPY transaction history
this.transactionHistory = new ArrayList<>();
for (Transaction transaction : original.transactionHistory) {
this.transactionHistory.add(new Transaction(transaction));
}
System.out.println("Account copy created: " + this.accountNumber);
}
// Business methods
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
transactionHistory.add(new Transaction("DEPOSIT", amount));
System.out.println("Deposited: $" + amount);
}
}
public void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
transactionHistory.add(new Transaction("WITHDRAWAL", amount));
System.out.println("Withdrawn: $" + amount);
} else {
System.out.println("Insufficient funds or invalid amount");
}
}
public void displayAccountInfo() {
System.out.println("=== ACCOUNT INFORMATION ===");
System.out.println("Account: " + accountNumber);
System.out.println("Holder: " + accountHolder);
System.out.println("Balance: $" + balance);
System.out.println("Transactions: " + transactionHistory.size());
System.out.println("---------------------------");
}
public void displayTransactionHistory() {
System.out.println("=== TRANSACTION HISTORY for " + accountNumber + " ===");
for (Transaction transaction : transactionHistory) {
System.out.println("  " + transaction);
}
System.out.println("=================================");
}
}
public class BankAccountCopyDemo {
public static void main(String[] args) {
System.out.println("=== BANK ACCOUNT COPY DEMO ===");
// Create original account
BankAccount originalAccount = new BankAccount("ACC123", "John Doe", 1000.0);
originalAccount.deposit(500.0);
originalAccount.withdraw(200.0);
System.out.println();
// Display original account
originalAccount.displayAccountInfo();
originalAccount.displayTransactionHistory();
System.out.println();
// Create copy at this point in time (snapshot)
BankAccount accountSnapshot = new BankAccount(originalAccount);
System.out.println();
// Continue transactions on original
originalAccount.deposit(300.0);
originalAccount.withdraw(100.0);
System.out.println();
// Display both accounts
System.out.println("=== AFTER MORE TRANSACTIONS ===");
originalAccount.displayAccountInfo();
originalAccount.displayTransactionHistory();
accountSnapshot.displayAccountInfo();
accountSnapshot.displayTransactionHistory();
// Demonstrate they are independent
System.out.println("=== INDEPENDENCE DEMONSTRATION ===");
accountSnapshot.deposit(1000.0); // Only affects the copy
System.out.println();
originalAccount.displayAccountInfo();
accountSnapshot.displayAccountInfo();
}
}

Output:

=== BANK ACCOUNT COPY DEMO ===
Account created: ACC123
Deposited: $500.0
Withdrawn: $200.0
=== ACCOUNT INFORMATION ===
Account: ACC123
Holder: John Doe
Balance: $1300.0
Transactions: 3
---------------------------
=== TRANSACTION HISTORY for ACC123 ===
OPENING: $1000.00 at Mon Jan 15 10:30:45 EST 2024
DEPOSIT: $500.00 at Mon Jan 15 10:30:45 EST 2024
WITHDRAWAL: $200.00 at Mon Jan 15 10:30:45 EST 2024
=================================
Account copy created: COPY-ACC123
Deposited: $300.0
Withdrawn: $100.0
=== AFTER MORE TRANSACTIONS ===
=== ACCOUNT INFORMATION ===
Account: ACC123
Holder: John Doe
Balance: $1500.0
Transactions: 5
---------------------------
=== TRANSACTION HISTORY for ACC123 ===
OPENING: $1000.00 at Mon Jan 15 10:30:45 EST 2024
DEPOSIT: $500.00 at Mon Jan 15 10:30:45 EST 2024
WITHDRAWAL: $200.00 at Mon Jan 15 10:30:45 EST 2024
DEPOSIT: $300.00 at Mon Jan 15 10:30:45 EST 2024
WITHDRAWAL: $100.00 at Mon Jan 15 10:30:45 EST 2024
=================================
=== ACCOUNT INFORMATION ===
Account: COPY-ACC123
Holder: John Doe
Balance: $1300.0
Transactions: 3
---------------------------
=== TRANSACTION HISTORY for COPY-ACC123 ===
OPENING: $1000.00 at Mon Jan 15 10:30:45 EST 2024
DEPOSIT: $500.00 at Mon Jan 15 10:30:45 EST 2024
WITHDRAWAL: $200.00 at Mon Jan 15 10:30:45 EST 2024
=================================
=== INDEPENDENCE DEMONSTRATION ===
Deposited: $1000.0
=== ACCOUNT INFORMATION ===
Account: ACC123
Holder: John Doe
Balance: $1500.0
Transactions: 5
---------------------------
=== ACCOUNT INFORMATION ===
Account: COPY-ACC123
Holder: John Doe
Balance: $2300.0
Transactions: 4
---------------------------

Example 4: Immutable Class with Copy Constructor

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
// Immutable class - once created, cannot be modified
final class ImmutablePerson {
private final String name;
private final int age;
private final List<String> hobbies;
// Regular constructor
public ImmutablePerson(String name, int age, List<String> hobbies) {
this.name = name;
this.age = age;
// Defensive copy - create new ArrayList from parameter
this.hobbies = new ArrayList<>(hobbies);
}
// ✅ COPY CONSTRUCTOR - creates copy of immutable object
public ImmutablePerson(ImmutablePerson original) {
this.name = original.name;
this.age = original.age;
this.hobbies = new ArrayList<>(original.hobbies); // Deep copy
}
// Only getters - no setters (immutable)
public String getName() { return name; }
public int getAge() { return age; }
// Return unmodifiable view to protect internal list
public List<String> getHobbies() {
return Collections.unmodifiableList(hobbies);
}
// "Modification" methods return new instances
public ImmutablePerson withName(String newName) {
return new ImmutablePerson(newName, this.age, this.hobbies);
}
public ImmutablePerson withAge(int newAge) {
return new ImmutablePerson(this.name, newAge, this.hobbies);
}
public ImmutablePerson withHobbyAdded(String newHobby) {
List<String> newHobbies = new ArrayList<>(this.hobbies);
newHobbies.add(newHobby);
return new ImmutablePerson(this.name, this.age, newHobbies);
}
@Override
public String toString() {
return String.format("Person{name='%s', age=%d, hobbies=%s}", 
name, age, hobbies);
}
}
public class ImmutableClassDemo {
public static void main(String[] args) {
System.out.println("=== IMMUTABLE CLASS WITH COPY CONSTRUCTOR ===");
// Create original hobbies list
List<String> hobbies = new ArrayList<>();
hobbies.add("Reading");
hobbies.add("Swimming");
// Create original immutable person
ImmutablePerson original = new ImmutablePerson("Alice", 25, hobbies);
System.out.println("Original: " + original);
// Create copy using copy constructor
ImmutablePerson copy = new ImmutablePerson(original);
System.out.println("Copy: " + copy);
// Try to modify the original hobbies list (won't affect immutable object)
hobbies.add("Hacking");
System.out.println("After modifying original hobbies list:");
System.out.println("Original: " + original); // Unchanged!
System.out.println("Copy: " + copy);         // Unchanged!
System.out.println();
// Use "with" methods to create modified copies
ImmutablePerson olderAlice = original.withAge(26);
ImmutablePerson aliceWithMusic = original.withHobbyAdded("Music");
System.out.println("Original: " + original);
System.out.println("Older: " + olderAlice);
System.out.println("With Music: " + aliceWithMusic);
System.out.println();
// Demonstrate that copies are independent
System.out.println("=== INDEPENDENCE DEMONSTRATION ===");
System.out.println("Original == Copy: " + (original == copy)); // Different objects
System.out.println("Original equals Copy: " + original.toString().equals(copy.toString()));
}
}

Output:

=== IMMUTABLE CLASS WITH COPY CONSTRUCTOR ===
Original: Person{name='Alice', age=25, hobbies=[Reading, Swimming]}
Copy: Person{name='Alice', age=25, hobbies=[Reading, Swimming]}
After modifying original hobbies list:
Original: Person{name='Alice', age=25, hobbies=[Reading, Swimming]}
Copy: Person{name='Alice', age=25, hobbies=[Reading, Swimming]}
Original: Person{name='Alice', age=25, hobbies=[Reading, Swimming]}
Older: Person{name='Alice', age=26, hobbies=[Reading, Swimming]}
With Music: Person{name='Alice', age=25, hobbies=[Reading, Swimming, Music]}
=== INDEPENDENCE DEMONSTRATION ===
Original == Copy: false
Original equals Copy: true

Example 5: Copy Constructor vs clone() Method

class Product implements Cloneable {
private String name;
private double price;
private String[] tags;
public Product(String name, double price, String[] tags) {
this.name = name;
this.price = price;
this.tags = tags; // Shallow copy
}
// ✅ COPY CONSTRUCTOR
public Product(Product original) {
this.name = original.name;
this.price = original.price;
// Deep copy for array
if (original.tags != null) {
this.tags = new String[original.tags.length];
System.arraycopy(original.tags, 0, this.tags, 0, original.tags.length);
} else {
this.tags = null;
}
}
// ❌ clone() method - often problematic
@Override
protected Object clone() throws CloneNotSupportedException {
Product cloned = (Product) super.clone();
// Still need to handle deep copy manually
if (this.tags != null) {
cloned.tags = this.tags.clone(); // Array.clone() creates shallow copy
}
return cloned;
}
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public String[] getTags() { return tags; }
public void setTags(String[] tags) { this.tags = tags; }
public void displayInfo() {
System.out.println("Product: " + name);
System.out.println("Price: $" + price);
System.out.println("Tags: " + java.util.Arrays.toString(tags));
System.out.println("---------------");
}
// Method to demonstrate modification
public void modifyFirstTag() {
if (tags != null && tags.length > 0) {
tags[0] = "MODIFIED";
}
}
}
public class CopyConstructorVsClone {
public static void main(String[] args) {
System.out.println("=== COPY CONSTRUCTOR vs clone() METHOD ===");
String[] tags = {"Electronics", "Gadget", "New"};
Product original = new Product("Smartphone", 699.99, tags);
System.out.println("ORIGINAL:");
original.displayInfo();
// Using copy constructor
Product copyConstructorCopy = new Product(original);
// Using clone method
Product cloneCopy = null;
try {
cloneCopy = (Product) original.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
System.out.println("BEFORE MODIFICATION:");
System.out.print("Copy Constructor: ");
copyConstructorCopy.displayInfo();
System.out.print("Clone: ");
if (cloneCopy != null) cloneCopy.displayInfo();
// Modify the original
original.modifyFirstTag();
original.setPrice(599.99);
System.out.println("AFTER MODIFYING ORIGINAL:");
System.out.print("Original: ");
original.displayInfo();
System.out.print("Copy Constructor: ");
copyConstructorCopy.displayInfo(); // Unaffected
System.out.print("Clone: ");
if (cloneCopy != null) cloneCopy.displayInfo(); // Might be affected!
System.out.println();
System.out.println("=== ADVANTAGES OF COPY CONSTRUCTOR ===");
System.out.println("1. More readable and explicit");
System.out.println("2. Better control over deep/shallow copy");
System.out.println("3. No need to handle CloneNotSupportedException");
System.out.println("4. Can be used for inheritance properly");
System.out.println("5. More maintainable and less error-prone");
}
}

Output:

=== COPY CONSTRUCTOR vs clone() METHOD ===
ORIGINAL:
Product: Smartphone
Price: $699.99
Tags: [Electronics, Gadget, New]
---------------
BEFORE MODIFICATION:
Copy Constructor: Product: Smartphone
Price: $699.99
Tags: [Electronics, Gadget, New]
---------------
Clone: Product: Smartphone
Price: $699.99
Tags: [Electronics, Gadget, New]
---------------
AFTER MODIFYING ORIGINAL:
Original: Product: Smartphone
Price: $599.99
Tags: [MODIFIED, Gadget, New]
---------------
Copy Constructor: Product: Smartphone
Price: $699.99
Tags: [Electronics, Gadget, New]
---------------
Clone: Product: Smartphone
Price: $699.99
Tags: [MODIFIED, Gadget, New]
---------------
=== ADVANTAGES OF COPY CONSTRUCTOR ===
1. More readable and explicit
2. Better control over deep/shallow copy
3. No need to handle CloneNotSupportedException
4. Can be used for inheritance properly
5. More maintainable and less error-prone

Types of Object Copying

TypeDescriptionCopy Constructor Approach
Shallow CopyCopies references to nested objectsthis.field = original.field
Deep CopyCreates new copies of nested objectsthis.field = new Type(original.field)
Lazy CopyCombination - copy on first modificationHybrid approach

When to Use Copy Constructors

✅ Use Copy Constructors When:

  • You need explicit object copying
  • Working with complex object graphs
  • Implementing immutable objects
  • Better control over copying behavior
  • Avoiding clone() method complexities

✅ Common Use Cases:

  • Creating snapshots of object state
  • Prototype pattern implementation
  • Caching systems with object copies
  • Thread-safe operations with local copies
  • Undo/redo functionality in applications

Best Practices

  1. Always document whether copy constructor does shallow or deep copy
  2. Use deep copy for mutable nested objects
  3. Consider immutability to avoid copying complexities
  4. Handle null values gracefully in copy constructors
  5. Use copy constructors instead of clone() for new code
  6. Test thoroughly to ensure independence of copies
// ✅ GOOD PRACTICE: Comprehensive copy constructor
public class GoodCopyExample {
private String data;
private List<String> items;
private Map<String, Object> properties;
public GoodCopyExample(GoodCopyExample original) {
// Copy primitive and immutable fields
this.data = original.data;
// Deep copy for mutable collections
this.items = (original.items != null) ? 
new ArrayList<>(original.items) : null;
this.properties = (original.properties != null) ?
new HashMap<>(original.properties) : null;
}
}

Common Pitfalls

class ProblematicCopy {
private int[] data;
private List<String> items;
// ❌ PROBLEM: Shallow copy of mutable fields
public ProblematicCopy(ProblematicCopy original) {
this.data = original.data;          // ❌ Shares array reference
this.items = original.items;        // ❌ Shares list reference
}
// ✅ SOLUTION: Proper deep copy
public ProblematicCopy(ProblematicCopy original, boolean deepCopy) {
if (deepCopy) {
// Deep copy array
if (original.data != null) {
this.data = new int[original.data.length];
System.arraycopy(original.data, 0, this.data, 0, original.data.length);
}
// Deep copy list
if (original.items != null) {
this.items = new ArrayList<>(original.items);
}
} else {
// Shallow copy (if explicitly requested)
this.data = original.data;
this.items = original.items;
}
}
}

Conclusion

Copy constructors are like precision photocopiers for your objects:

  • Create independent copies of existing objects
  • Explicit and readable - clearly shows copying intent
  • Flexible control over shallow vs deep copying
  • Better alternative to clone() method
  • Essential for immutable objects and snapshot patterns

Key Takeaways:

  • Copy constructors initialize new objects from existing ones
  • Deep copy creates completely independent objects
  • Shallow copy shares references to nested objects
  • Always consider mutability when designing copy behavior
  • Prefer copy constructors over clone() for new code

Copy constructors give you precise control over object duplication, making them invaluable for creating robust, maintainable Java applications where object copying is required!

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper