Constructors

Enforcing valid object state from the moment of creation

← Back to Index

The Problem Constructors Solve

Every object needs to start in a valid state. A BankAccount without an ID is meaningless. A User with a null email will crash the first time someone tries to send a confirmation. A Connection with no host cannot connect to anything. If you leave object initialisation to the caller — setting fields manually after creation — nothing guarantees they'll do it correctly, or at all.

// Without a constructor: caller is responsible for initialisation
            // Nothing stops them from forgetting, or setting invalid values
            public class BankAccount {
                public String id;
                public double balance;
            }
            
            BankAccount acc = new BankAccount();
            // id is null, balance is 0.0 — object exists but is completely unusable
            // caller might forget to set id, or set balance = -500, and nothing stops them
            acc.balance = -500;  // perfectly legal, completely wrong
            
            // With a constructor: the class controls its own initialisation
            public class BankAccount {
                private final String id;
                private       double balance;
            
                public BankAccount(String id, double initialBalance) {
                    if (id == null || id.isBlank())
                        throw new IllegalArgumentException("ID required");
                    if (initialBalance < 0)
                        throw new IllegalArgumentException("Balance cannot be negative");
                    this.id      = id;
                    this.balance = initialBalance;
                }
            }
            
            // Now there is no way to create an invalid BankAccount:
            // new BankAccount(null, 0);   → IllegalArgumentException
            // new BankAccount("", -500);  → IllegalArgumentException
            // new BankAccount("ACC-001", 500); → valid, guaranteed
The invariant guarantee

An invariant is a condition that must always be true about an object — "the ID is never null", "the balance is never negative". Constructors are where you establish invariants. Methods like withdraw() then maintain them. If the constructor enforces the invariant correctly, the object can never be in an invalid state — because it never escapes the constructor in one.

What is a Constructor?

A constructor is a special method that runs automatically when you create an object with new. Its job: put the object into a valid state before anyone can use it. An object should never exist in an invalid state — constructors are the enforcement mechanism for that rule.

Constructors differ from regular methods in three ways: same name as the class, no return type (not even void), and called automatically by new — you never invoke them directly.

public class BankAccount {
    private final String id;
    private       double balance;

    // Constructor: same name as class, no return type
    public BankAccount(String id, double initialBalance) {
        if (id == null || id.isBlank())
            throw new IllegalArgumentException("ID required");
        if (initialBalance < 0)
            throw new IllegalArgumentException("Balance cannot be negative");

        this.id      = id;
        this.balance = initialBalance;
    }

    public double getBalance() { return balance; }
}

// new triggers: memory allocation → field defaults → constructor body
BankAccount acc = new BankAccount("ACC-001", 500.0);
// new BankAccount(null, -1);  // throws immediately — object never escapes invalid
Constructor rules
  • Name must match the class exactly (case-sensitive)
  • No return type — not even void
  • Can be overloaded (multiple constructors, different parameters)
  • Not inherited — subclasses must define their own and call super()
  • If you define no constructor, Java provides a free no-arg one. Define any constructor and the free one disappears.

Object Creation Under the Hood

Knowing the exact sequence prevents a class of subtle bugs — especially with inheritance and final fields.

/*
 *  What happens when you write: new Dog("Buddy", 3)
 *
 *  1. JVM allocates memory on the heap
 *     All fields set to defaults first (null / 0 / false)
 *
 *  2. Instance initializer blocks run (in textual order)
 *
 *  3. Constructor body executes
 *     Fields get their real values
 *
 *  4. Reference returned to the caller
 *     → myDog points to the fully-initialised object
 *
 *  In bytecode, new Dog("Buddy", 3) compiles to:
 *    new          ← allocate
 *    dup          ← duplicate reference
 *    ldc "Buddy"  ← push arg
 *    iconst_3     ← push arg
 *    invokespecial Dog.<init>(String, int)  ← call constructor
 *    astore_1     ← store in variable
 */

Complete initialization order

public class InitOrder {
    // 1. Static fields + static blocks — ONCE per class load
    private static int classCount = 0;
    static { System.out.println("static block"); }

    // 2. Instance fields — per object, before constructor
    private final String tag = "tag-" + ++classCount;

    // 3. Instance initializer — per object, before constructor
    { System.out.println("instance init, tag=" + tag); }

    // 4. Constructor body
    public InitOrder() { System.out.println("constructor"); }
}

new InitOrder();
// static block        ← only first time
// instance init, tag=tag-1
// constructor

new InitOrder();
// instance init, tag=tag-2   ← static block doesn't repeat
// constructor

Constructor Types

Default constructor (compiler-generated)

public class Config {
    private String host = "localhost";
    // No constructor declared → compiler generates: public Config() { }
}

Config c = new Config(); // works

// ⚠️ Define any constructor and the default disappears:
public class Config {
    public Config(String host) { ... }
}
new Config();  // ERROR — no longer exists
// Fix: explicitly add public Config() { this("localhost"); }

Parameterized constructor — validate on entry

public class Person {
    private final String name;
    private final int    birthYear;
    private       String email;

    // Primary constructor — ALL validation lives here
    public Person(String name, int birthYear) {
        if (name == null || name.isBlank())
            throw new IllegalArgumentException("Name required");
        if (birthYear < 1900 || birthYear > 2026)
            throw new IllegalArgumentException("Invalid birth year");
        this.name      = name;
        this.birthYear = birthYear;
    }

    // Secondary constructor delegates — no duplicated validation
    public Person(String name, int birthYear, String email) {
        this(name, birthYear);  // this() must be the first line
        this.email = email;
    }
}

Copy constructor

public class Team {
    private String       name;
    private List<String> members;

    public Team(String name, List<String> members) {
        this.name    = name;
        this.members = new ArrayList<>(members); // defensive copy
    }

    // Copy constructor — deep copy so changes to one don't affect the other
    public Team(Team other) {
        this.name    = other.name;                      // String: immutable, safe to share
        this.members = new ArrayList<>(other.members); // List: must copy
    }
}

Team original = new Team("Alpha", List.of("Alice", "Bob"));
Team copy     = new Team(original);  // independent object

Constructor Chaining

this() calls another constructor in the same class. super() calls a constructor in the parent class. Both must be the first statement in the constructor — you cannot have both in the same constructor.

this() — delegate within the class

public class Employee {
    private final String name;
    private final int    id;
    private final double salary;
    private final String department;

    // PRIMARY constructor — validation and assignment in one place
    public Employee(String name, int id, double salary, String department) {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("Name required");
        if (salary < 0)                   throw new IllegalArgumentException("Salary >= 0");
        this.name       = name;
        this.id         = id;
        this.salary     = salary;
        this.department = department;
    }

    // Convenience overloads — chain to primary, no duplicated logic
    public Employee(String name, int id, double salary) {
        this(name, id, salary, "Unassigned");
    }
    public Employee(String name, int id) {
        this(name, id, 50_000.0);
    }
    public Employee(String name) {
        this(name, generateId());
    }

    private static int nextId = 1000;
    private static int generateId() { return nextId++; }
}

// Employee("Alice") → this("Alice", 1000) → this("Alice", 1000, 50000) →
//   this("Alice", 1000, 50000, "Unassigned") → validation + assignment

super() — call the parent constructor

public class Animal {
    protected final String name;
    protected final int    age;

    public Animal(String name, int age) {
        if (name == null) throw new IllegalArgumentException();
        this.name = name;
        this.age  = age;
    }
}

public class Dog extends Animal {
    private final String breed;

    public Dog(String name, int age, String breed) {
        super(name, age);   // ← MUST be first; parent validates name/age
        this.breed = breed;
    }
}

// Execution order for new Dog("Buddy", 3, "Labrador"):
//   Animal constructor runs first (validates + sets name, age)
//   Dog constructor body continues (sets breed)

// Multi-level: Animal → Mammal → Dog constructors run root-first
// If Dog doesn't call super(), compiler inserts super() — and errors
// if Animal has no no-arg constructor.

Private Constructors and Creation Patterns

Private constructors give you complete control over how (and whether) objects are created. Three main uses:

Static factory methods — named, semantic constructors

public final class Color {
    private final int r, g, b;

    private Color(int r, int g, int b) { this.r = r; this.g = g; this.b = b; }

    // Named factories — intent is clear, validation centralised
    public static Color of(int r, int g, int b) {
        if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255)
            throw new IllegalArgumentException("Values must be 0-255");
        return new Color(r, g, b);
    }
    public static Color ofHex(String hex) {
        return new Color(
            Integer.parseInt(hex.substring(1, 3), 16),
            Integer.parseInt(hex.substring(3, 5), 16),
            Integer.parseInt(hex.substring(5, 7), 16)
        );
    }
    public static final Color RED   = new Color(255, 0,   0);
    public static final Color GREEN = new Color(0,   255, 0);

    // Static factory can also CACHE instances — impossible with constructors
}

Color c1 = Color.of(255, 128, 0);
Color c2 = Color.ofHex("#FF8000");

Utility class — prevent instantiation

public final class MathUtils {
    private MathUtils() {
        throw new AssertionError("Utility class");
    }
    public static boolean isPrime(int n) { ... }
}

Builder pattern — many optional parameters

// ❌ Telescoping constructors — hard to read, easy to swap args by mistake
new HttpRequest("https://api.example.com", "POST", headers, body, 30000, true);

// ✅ Builder — readable, named, optional fields have defaults
public class HttpRequest {
    private final String  url;
    private final String  method;
    private final String  body;
    private final int     timeout;

    // Private constructor — only Builder can call it
    private HttpRequest(Builder b) {
        this.url     = b.url;
        this.method  = b.method;
        this.body    = b.body;
        this.timeout = b.timeout;
    }

    public static class Builder {
        private final String url;           // required
        private       String method  = "GET";  // defaults
        private       String body    = null;
        private       int    timeout = 30_000;

        public Builder(String url) { this.url = url; }

        public Builder method(String m) { this.method = m;  return this; }
        public Builder body(String b)   { this.body = b;    return this; }
        public Builder timeout(int ms)   { this.timeout = ms; return this; }

        public HttpRequest build() {
            if (url == null || url.isBlank()) throw new IllegalStateException("URL required");
            return new HttpRequest(this);
        }
    }
}

HttpRequest req = new HttpRequest.Builder("https://api.example.com")
    .method("POST")
    .body("{ \"key\": \"value\" }")
    .timeout(5_000)
    .build();

Common Pitfalls

Calling overridable methods from a constructor
// ❌ DANGEROUS: subclass method called before subclass fields are initialised
public abstract class Base {
    public Base() {
        init();  // runs Child.init() — but Child's fields aren't set yet!
    }
    protected abstract void init();
}

public class Child extends Base {
    private final String value = "hello";
    @Override
    protected void init() {
        System.out.println(value.toUpperCase()); // NullPointerException!
    }                                             // value is null here
}

// ✅ Fix: use a factory method, or explicit initialise() called by caller
// Never call overridable methods from constructors.
final field not set in all constructor paths
public class Config {
    private final String value;

    public Config(boolean useDefault) {
        if (useDefault) value = "default";
        // ❌ ERROR: value not assigned when useDefault is false
    }

    // ✅ Fix: all paths must assign final fields
    public Config(boolean useDefault) {
        value = useDefault ? "default" : "custom";
    }
}
Circular constructor chain
public MyClass()        { this(0); }  // calls MyClass(int)
public MyClass(int x)    { this();   }  // calls MyClass() ← infinite loop
// Compile error: recursive constructor invocation

Senior Topics: Records and Dependency Injection

Records as the modern constructor alternative (Java 16+)

For immutable data carriers, record generates the canonical constructor, accessors, equals, hashCode, and toString automatically. Less boilerplate, same guarantees.

// ❌ Old way: 30+ lines of boilerplate
public final class Money {
    private final BigDecimal amount;
    private final String     currency;
    // constructor, getters, equals, hashCode, toString...
}

// ✅ Record: one line, same behaviour
record Money(BigDecimal amount, String currency) {

    // Compact constructor — add validation without re-listing params
    Money {
        if (amount == null)   throw new IllegalArgumentException("Amount required");
        if (amount.compareTo(BigDecimal.ZERO) < 0)
            throw new IllegalArgumentException("Negative amount");
        // Fields are assigned automatically after this block
    }

    // Can add static factories and custom methods
    public static Money euros(BigDecimal amount) {
        return new Money(amount, "EUR");
    }
}

Money m = Money.euros(new BigDecimal("9.99"));
m.amount();    // accessor (no "get" prefix)
m.currency();  // accessor

Constructor injection — the right way to build objects in Spring/CDI

// ❌ Field injection — breaks immutability, hides dependencies, untestable
@Service
public class OrderService {
    @Autowired private OrderRepository repo;      // mutable, nullable
    @Autowired private PaymentProcessor payments;  // hidden dependency
}

// ✅ Constructor injection — fields are final, dependencies explicit, unit-testable
@Service
public class OrderService {
    private final OrderRepository repo;
    private final PaymentProcessor payments;

    // Spring injects via constructor (single constructor = no @Autowired needed)
    public OrderService(OrderRepository repo, PaymentProcessor payments) {
        this.repo     = Objects.requireNonNull(repo);
        this.payments = Objects.requireNonNull(payments);
    }

    // In unit tests, inject mocks directly — no Spring context needed
    // new OrderService(mockRepo, mockPayments);
}
Constructor injection vs field injection — why it matters

Constructor injection forces you to make dependencies explicit and enables final fields. An object built with a valid constructor is guaranteed to be fully initialised — no NPE from a missing @Autowired that the container forgot to fill. Field injection also makes unit testing painful because you need reflection or a DI container just to set up the object. Constructor injection works with plain new in tests.

Interview Questions

🎓 Junior level

Q: What is the difference between a constructor and a method?
Constructors have the same name as the class, no return type, and are called automatically by new. Methods have any name, a return type, and are called explicitly. Constructors are not inherited; methods can be. Purpose: constructors initialise; methods operate.

Q: What happens if you don't define any constructor?
The compiler generates a public no-arg constructor equivalent to public MyClass() { super(); }. As soon as you define any constructor yourself, the generated one disappears — a frequent source of "no suitable constructor" errors when frameworks try to call the no-arg one.

Q: What is constructor chaining and why is it useful?
this() delegates to another constructor in the same class. super() delegates to the parent. Benefits: validation and initialization live in one place (DRY), all overloads are consistent, and changes propagate automatically. The chain must be acyclic — the compiler detects circular chains at compile time.

Q: What is the initialization order when creating an object?
(1) Static fields + static blocks, once per class load. (2) Instance field initializers. (3) Instance initializer blocks. (4) Constructor body. For inheritance, the parent's steps 2-4 run before the child's.

🔥 Senior level

Q: Why prefer static factory methods over constructors?
Three reasons: (1) They have names — Color.ofHex("#FF0000") is clearer than new Color("#FF0000"). (2) They can return cached instances — Boolean.valueOf(true) always returns the same object; new Boolean(true) always creates a new one (now deprecated for this reason). (3) They can return a subtype — List.of() returns an internal implementation class; callers only see List. See Effective Java Item 1.

Q: Why is calling an overridable method from a constructor dangerous?
When new Child() runs, the JVM first calls the Base constructor. If that constructor calls init(), which is overridden in Child, the Child.init() executes before Child's own constructor body — meaning all of Child's final fields are still at their defaults (null, 0). The result is NPE on a field that looks initialised in the source. Rule: never call overridable (non-final, non-private) methods from constructors.

Q: When should you use Builder instead of telescoping constructors?
When the class has more than 3-4 parameters, especially if several are optional or of the same type (easy to swap accidentally). Builder gives named parameters, default values, and multi-step validation in build(). It also allows the final object to be immutable (all fields final) while the builder is mutable during construction. Lombok's @Builder and records eliminate the boilerplate in modern Java.

Q: What is the advantage of constructor injection over field injection in Spring?
Constructor injection enables final fields (guaranteed initialisation, immutability), makes dependencies explicit in the signature (visible at a glance), and allows unit testing without a Spring context — just new MyService(mockRepo). Field injection hides dependencies, prevents final, and requires reflection or a container to set values. Spring recommends constructor injection since 4.x; it's the default when only one constructor exists (no @Autowired needed).