☕ Java

Copy Constructor

A copy constructor creates a new object as a duplicate of an existing object of the same type. It takes a single parameter of the same class type and copies the source object's fields into the new instance. Java does not provide a copy constructor automatically — unlike C++, which generates one by default — so it must be written explicitly. Copy constructors are the idiomatic Java way to duplicate objects, giving precise control over whether the copy is shallow (shared references) or deep (independent copies of all nested objects).

Why Copy Constructors Are Needed

In Java, assigning one object reference to another variable does not copy the object — it copies the reference. Both variables then point to the same object in memory, so changes through one variable are immediately visible through the other. This is called aliasing and it is a common source of bugs when the programmer intended to work with an independent copy. The need for a true copy arises in several situations. When passing an object to a method that might modify it, you want to pass a copy rather than the original. When returning an object from a method, you want to return a defensive copy rather than a reference to your internal state. When storing an object that was passed in from outside, you want to store a copy rather than the caller's reference, which they might modify later. Java does provide Object.clone(), but it is widely considered a design mistake. It requires implementing a Cloneable marker interface, its return type is Object (requiring a cast), it throws a checked exception, it performs a shallow copy by default, and it has unusual semantics around inheritance. The copy constructor is universally preferred by experienced Java developers — it is explicit, type-safe, easy to understand, and gives complete control over what "copy" means for a specific class.
Java
// ── Assignment copies reference — NOT the object: ────────────────────
int[] original = {1, 2, 3, 4, 5};
int[] alias    = original;          // alias points to the SAME array

alias[0] = 99;
System.out.println(original[0]);    // 99 — original was modified through alias!

// ── Same problem with objects: ────────────────────────────────────────
public class Config {
    private String host;
    private int    port;

    public Config(String host, int port) {
        this.host = host;
        this.port = port;
    }
    public void setPort(int port) { this.port = port; }
    public String toString() { return host + ":" + port; }
}

Config prod = new Config("prod.example.com", 8080);
Config test = prod;     // test points to the SAME Config object

test.setPort(9090);
System.out.println(prod);   // prod.example.com:9090  ← CHANGED!
// We changed "test" but prod changed too — they are the same object.

// ── Copy constructor creates an independent duplicate: ────────────────
public class Config {
    private String host;
    private int    port;

    public Config(String host, int port) {
        this.host = host;
        this.port = port;
    }

    // Copy constructor:
    public Config(Config other) {
        this.host = other.host;
        this.port = other.port;
    }

    public void setPort(int port) { this.port = port; }
    public String toString() { return host + ":" + port; }
}

Config prod  = new Config("prod.example.com", 8080);
Config test  = new Config(prod);    // NEW independent object

test.setPort(9090);
System.out.println(prod);   // prod.example.com:8080  ← unchanged
System.out.println(test);   // prod.example.com:9090  ← only test changed

Shallow Copy vs Deep Copy

A shallow copy creates a new object but copies only the top-level field values. For primitive fields (int, double, boolean), the value itself is copied and the copy is completely independent. For reference fields (String, List, arrays, other objects), only the reference is copied — both the original and the copy point to the same nested object. This means modifying the nested object through one reference is visible through the other. A deep copy creates a new object and also recursively creates new copies of every referenced object. The result is a completely independent copy — modifying any part of the deep copy has no effect on the original at any level of nesting. Which is appropriate depends on the class's design. If the fields hold references to immutable objects (String, Integer, LocalDate, BigDecimal), a shallow copy is safe because the referenced objects cannot be modified — both the original and copy share the same immutable object, but that is harmless. If the fields hold references to mutable objects (ArrayList, HashMap, arrays, custom mutable classes), a deep copy is needed to ensure independence. Most production copy constructors handle both: they copy primitives and immutable references directly, and call copy constructors or copy methods on mutable referenced objects.
Java
// ── Shallow copy — shares mutable field references: ──────────────────
public class CourseEnrollment {
    private String       studentName;
    private List<String> courses;           // mutable — shared in shallow copy

    public CourseEnrollment(String studentName, List<String> courses) {
        this.studentName = studentName;
        this.courses     = courses;
    }

    // SHALLOW copy constructor — shares the list reference:
    public CourseEnrollment(CourseEnrollment other) {
        this.studentName = other.studentName;   // String is immutable — safe
        this.courses     = other.courses;       // SHALLOW — same List object!
    }

    public void addCourse(String course) { courses.add(course); }
    public List<String> getCourses()     { return courses; }
    public String toString() { return studentName + ": " + courses; }
}

CourseEnrollment alice = new CourseEnrollment("Alice",
    new ArrayList<>(List.of("Maths", "Physics")));
CourseEnrollment copy  = new CourseEnrollment(alice);   // shallow copy

copy.addCourse("Chemistry");
System.out.println(alice);  // Alice: [Maths, Physics, Chemistry]  ← CHANGED!
System.out.println(copy);   // Alice: [Maths, Physics, Chemistry]
// Both share the same List — change in copy affects original.

// ── Deep copy — independent nested objects: ───────────────────────────
public class CourseEnrollment {
    private String       studentName;
    private List<String> courses;

    public CourseEnrollment(String studentName, List<String> courses) {
        this.studentName = studentName;
        this.courses     = new ArrayList<>(courses);    // defensive copy on construction
    }

    // DEEP copy constructor — new List created:
    public CourseEnrollment(CourseEnrollment other) {
        this.studentName = other.studentName;           // String immutable — safe
        this.courses     = new ArrayList<>(other.courses);  // DEEP — new List
    }

    public void addCourse(String course) { courses.add(course); }
    public List<String> getCourses()     { return new ArrayList<>(courses); }
    public String toString() { return studentName + ": " + courses; }
}

CourseEnrollment alice2 = new CourseEnrollment("Alice",
    new ArrayList<>(List.of("Maths", "Physics")));
CourseEnrollment copy2  = new CourseEnrollment(alice2);  // deep copy

copy2.addCourse("Chemistry");
System.out.println(alice2);  // Alice: [Maths, Physics]  ← unchanged
System.out.println(copy2);   // Alice: [Maths, Physics, Chemistry]  ← only copy changed

Writing a Complete Copy Constructor

A complete copy constructor handles every field in the class: primitives are copied by value, immutable references (String, Integer, LocalDate, BigDecimal, enums) are copied by reference safely, and mutable references (arrays, collections, mutable custom classes) require new copies. When a class has a field of another custom class type, and that class has its own copy constructor, you call it: this.address = new Address(other.address). The copy constructor should also call the superclass copy constructor using super(other) if the class extends another class — otherwise the superclass fields will not be copied. If the superclass does not define a copy constructor, you need to copy its accessible fields manually or use getter methods. A well-written copy constructor is also where you validate that the source object to copy is not null, since null.field would immediately throw a NullPointerException with an unhelpful message.
Java
// ── Complete copy constructor — all field types handled: ─────────────
public class Employee {
    // Primitive fields:
    private int    id;
    private double salary;
    private boolean active;

    // Immutable reference — safe to share:
    private String     name;
    private LocalDate  hireDate;        // immutable — no copy needed

    // Mutable array — must copy:
    private int[]      performanceScores;

    // Mutable collection — must copy:
    private List<String> skills;

    // Custom mutable class — copy constructor of Address called:
    private Address    address;

    // Enum — immutable, safe to share:
    private Department department;

    // Primary constructor:
    public Employee(int id, String name, double salary, boolean active,
                    LocalDate hireDate, int[] performanceScores,
                    List<String> skills, Address address,
                    Department department) {
        this.id                = id;
        this.name              = name;          // String is immutable
        this.salary            = salary;
        this.active            = active;
        this.hireDate          = hireDate;      // LocalDate is immutable
        this.performanceScores = Arrays.copyOf(  // array — defensive copy
            performanceScores, performanceScores.length);
        this.skills            = new ArrayList<>(skills);  // list — defensive copy
        this.address           = new Address(address);     // custom deep copy
        this.department        = department;    // enum — immutable
    }

    // Copy constructor — mirrors primary constructor's copy strategy:
    public Employee(Employee other) {
        Objects.requireNonNull(other, "source employee cannot be null");
        this.id                = other.id;
        this.name              = other.name;            // String — safe
        this.salary            = other.salary;
        this.active            = other.active;
        this.hireDate          = other.hireDate;        // LocalDate — safe
        this.performanceScores = Arrays.copyOf(         // array — new copy
            other.performanceScores, other.performanceScores.length);
        this.skills            = new ArrayList<>(other.skills); // list — new copy
        this.address           = new Address(other.address);    // deep copy
        this.department        = other.department;      // enum — safe
    }

    // Getters return defensive copies of mutable fields:
    public int[]        getPerformanceScores() {
        return Arrays.copyOf(performanceScores, performanceScores.length);
    }
    public List<String> getSkills() { return new ArrayList<>(skills); }
    public Address      getAddress() { return new Address(address); }
    // Primitives and immutables returned directly:
    public int          getId()         { return id; }
    public String       getName()       { return name; }
    public double       getSalary()     { return salary; }
}

// ── Address class with its own copy constructor: ──────────────────────
public class Address {
    private String street;
    private String city;
    private String postcode;

    public Address(String street, String city, String postcode) {
        this.street   = street;
        this.city     = city;
        this.postcode = postcode;
    }

    // Copy constructor for Address:
    public Address(Address other) {
        Objects.requireNonNull(other, "source address cannot be null");
        this.street   = other.street;    // String — immutable
        this.city     = other.city;
        this.postcode = other.postcode;
    }
}

Copy Constructor vs Cloneable vs Record

Java developers have three main options for creating copies of objects: the copy constructor, the Cloneable interface with clone(), and Java records (which are inherently immutable and require no copying). Each has different trade-offs. The Cloneable approach has well-documented problems. The Cloneable interface has no clone() method in it — clone() is in Object, and Cloneable simply enables it. The clone() method is protected in Object, requiring each class to override it and make it public if cloning is needed. It performs a shallow copy by default. It throws CloneNotSupportedException even though this exception is never thrown for Cloneable classes — a design flaw. And it bypasses constructors entirely, which means validation logic and defensive copying in constructors is skipped. For these reasons, Joshua Bloch's Effective Java (the definitive guide to Java best practices) recommends against implementing Cloneable and using copy constructors instead. Java records, introduced in Java 16, provide a different approach. Records are immutable by design — all fields are final and set in the constructor. Because they cannot be modified after creation, sharing a record reference is equivalent to sharing a copy. Records have no copy constructor because copies are never needed — you simply use the same reference.
Java
// ── Copy constructor (recommended approach): ─────────────────────────
public class Config {
    private String host;
    private int    port;

    public Config(String host, int port) {
        this.host = host;
        this.port = port;
    }

    // Copy constructor — explicit, type-safe, calls our constructor:
    public Config(Config other) {
        Objects.requireNonNull(other, "source config cannot be null");
        this.host = other.host;
        this.port = other.port;
    }

    public void setPort(int port) { this.port = port; }
    public String toString() { return host + ":" + port; }
}

Config original = new Config("localhost", 8080);
Config copy     = new Config(original);           // clean, readable
copy.setPort(9090);
System.out.println(original);   // localhost:8080  ← unchanged
System.out.println(copy);       // localhost:9090

// ── Cloneable (avoid in new code — shown for comparison): ────────────
public class CloneableConfig implements Cloneable {
    private String host;
    private int    port;

    public CloneableConfig(String host, int port) {
        this.host = host;
        this.port = port;
    }

    @Override
    public CloneableConfig clone() {
        try {
            return (CloneableConfig) super.clone();  // cast needed — Object return type
        } catch (CloneNotSupportedException e) {
            throw new AssertionError("Never thrown for Cloneable", e);
        }
    }
}
// Problems: cast required, checked exception even though never thrown,
// shallow copy (must add manual deep-copy logic), bypasses constructor.

// ── Record — no copying needed at all: ────────────────────────────────
public record ImmutableConfig(String host, int port) {
    // Compact constructor for validation:
    public ImmutableConfig {
        Objects.requireNonNull(host, "host required");
        if (port < 1 || port > 65535)
            throw new IllegalArgumentException("Invalid port: " + port);
    }
    // No copy constructor needed — records are immutable.
    // Sharing a reference IS sharing a copy — state never changes.
}

ImmutableConfig cfg1 = new ImmutableConfig("localhost", 8080);
ImmutableConfig cfg2 = cfg1;   // both point to same record — that is fine
// cfg2 cannot modify cfg1's state — it has no setters.
System.out.println(cfg1);  // ImmutableConfig[host=localhost, port=8080]

// ── Comparison summary: ───────────────────────────────────────────────
//
// Copy constructor:
//   + Explicit — easy to read and understand
//   + Calls constructors — validation logic runs
//   + Full control over shallow vs deep
//   + No extra interface or checked exception
//   ○ Must be written manually
//
// Cloneable + clone():
//   - Bypasses constructors
//   - Shallow by default — requires extra work for deep
//   - Requires cast on every call
//   - Spurious checked exception (CloneNotSupportedException)
//   - Complex inheritance semantics
//   ✗ Avoid in new code
//
// Record (Java 16+):
//   + Immutable — copying is never needed
//   + Concise syntax
//   + equals(), hashCode(), toString() generated
//   ○ All fields must be final — not suitable for mutable objects

Related Topics in Object-Oriented Programming

OOP Concepts
Object-Oriented Programming (OOP) is a programming paradigm that organises software around objects — self-contained units that combine data (fields) and behaviour (methods). Java is a class-based, object-oriented language where almost everything is an object. OOP provides four foundational principles: encapsulation, inheritance, polymorphism, and abstraction. Together they produce software that is modular, reusable, maintainable, and easier to reason about as systems grow in complexity.
Classes
A class is the fundamental building block of Java. It is a blueprint that defines the structure and behaviour of objects — what data each object holds (fields) and what operations it can perform (methods). Every Java program is composed of classes. Understanding how to design a class well — choosing the right access modifiers, separating state from behaviour, and writing cohesive single-responsibility classes — is the foundation of object-oriented programming in Java. This entry covers class anatomy, fields, methods, access modifiers, static vs instance members, the this keyword, and class design principles.
Objects
An object is a runtime instance of a class. Where a class is a blueprint that exists in source code, an object is a living entity that exists in memory during program execution — it has its own identity, its own state stored in its fields, and the ability to respond to method calls. Every object has three fundamental properties: identity (a unique memory address), state (the current values of its fields), and behaviour (the methods it responds to). This entry covers object identity vs equality, the Object class hierarchy, object state and mutation, method calls, toString, equals and hashCode, and the object lifecycle.
Object Creation
Object creation in Java is the process of allocating memory, initialising fields, and running constructor logic to bring an object into existence. The new keyword is the primary mechanism, but Java also provides factory methods, builder patterns, copy constructors, and object cloning. Constructors are special methods that set up the initial state — their design determines how easy or difficult the class is to use correctly. This entry covers constructors in depth, constructor overloading and chaining, copy constructors, factory methods, the builder pattern, and the difference between shallow and deep copy.