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
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
- 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
// ❌ 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.
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";
}
}
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 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
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.
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).