Constructors in Java
Master Java constructors: default, parameterized, overloading, and constructor chaining with this() and super().
Master Java constructors: default, parameterized, overloading, and constructor chaining with this() and super().
Constructors in Java
Constructors are the gates through which objects enter existence. They initialize the raw memory that new allocates and set up the invariants your class depends on.
Introduction
Constructors are the initialization gate through which every Java object passes at creation time. When new allocates raw memory, the constructor runs to transform that memory into a valid object — setting required fields, validating parameters, establishing invariants, and invoking any parent class initialization via super(). Getting this wrong produces objects that exist in invalid states, causing bugs that are difficult to trace because the corruption happens silently before any code can check the object’s health. A constructor that accepts negative values for a balance field creates an account that looks valid until someone tries to process a transaction.
The mechanics carry specific constraints that catch developers unfamiliar with them. this() and super() — the constructor chaining calls — must be the first statement in any constructor body. If you omit an explicit call and the parent class has a no-arg constructor, Java inserts super() implicitly; but if the parent only has parameterized constructors, your code fails to compile with no diagnostic beyond “constructor call required.” Calling one constructor from another via this(args) enables constructor overloading with delegation rather than duplication, but recursive calls (A calling B calling A) are a compile-time error, not a runtime exception.
The most dangerous constructor anti-pattern is the this-escape: passing this to another object before the constructor finishes. A listener registered in a constructor can see the partially initialized object if that listener fires before the constructor completes. If the listener calls back into the object, it sees fields at their default values, not the values the constructor was in the process of setting. This post covers default and parameterized constructors, constructor chaining via this(), the super() call for parent initialization, copy constructors for cloning, why this() must be first, and the security-conscious pattern of defensive copying for mutable parameters.
When to Use
Use constructors when:
- Mandatory fields must be set at creation time — use a constructor that requires them
- Validating object creation — reject invalid states before the object exists
- Setting up dependencies — inject collaborators that the object needs
- Providing convenience — multiple constructors for different initialization paths
public class Player {
private final String username;
private int health;
private int score;
// Primary constructor — requires username
public Player(String username) {
this.username = username;
this.health = 100;
this.score = 0;
}
// Convenience constructor — delegates to primary with defaults
public Player(String username, int health) {
this(username); // Delegation via this()
this.health = health;
}
}
When Not to Use
Avoid constructors for:
- Complex object creation — use the Builder pattern instead
- Creating objects with many optional parameters — too many constructor overloads
- Creating immutable objects with many fields — Builder or factory methods
- When factory methods better express intent —
Card.of(rank, suit)vsnew Card(rank, suit)
// Too many parameters — error-prone for callers
public Config(String host, int port, String user, String pass, boolean ssl, int timeout, boolean retries);
// Better: Builder pattern
public class ConfigBuilder {
public ConfigBuilder host(String host) { ... return this; }
public ConfigBuilder port(int port) { ... return this; }
public Config build() { return new Config(this); }
}
Constructor Overloading — Mermaid Diagram
classDiagram
class Player {
+Player(String username)$
+Player(String username, int health)$
+Player(String username, int health, int score)$
+Player(Player other)$
}
note for Player "Constructor Chaining\nthis() -> this(username) -> this(username, health)"
Failure Scenarios
1. Forgetting to Call this() or super()
Java adds an implicit super() call for you — but only when the parent class has a no-arg constructor. The moment a parent declares any constructor, Java assumes you meant to take control of initialization and stops generating the default. This means a derived class with no explicit constructor call will fail to compile if its parent only has parameterized constructors. The compiler error points at the call site with no mention of the real cause: the missing super().
The fix is to call super(name) explicitly when the parent requires parameters. If the parent has multiple constructors, pick the one that matches the state your subclass needs to inherit. The example below shows a Base class with a single parameterized constructor and a Derived class that fails to compile without an explicit super(name) call.
public class Base {
public Base(String name) { }
}
public class Derived extends Base {
public Derived() {
// super(); // IMPLICIT — but only if Base has no-arg constructor
// If Base only has parameterized constructor, this fails
}
}
// Fix: explicitly call super(name)
public class Derived extends Base {
public Derived(String name) {
super(name); // Explicit call required
}
}
2. Constructor Calling Constructor (Infinite Loop)
this() enables constructor delegation, but it comes with a hard constraint: you cannot chain recursively. If constructor A calls this(args) which routes to constructor B, and constructor B calls this(args) which routes back to A, the compiler detects the cycle and rejects the code. Unlike infinite loops in regular methods which produce a stack overflow at runtime, constructor cycles are caught at compile time — Java treats them as a structural error rather than a runtime hazard.
The cycle does not have to be direct. A calling B calling C calling A is equally invalid. The compiler performs a static analysis of constructor call graphs and flags any path that loops back to a constructor already on the call stack. The fix is to eliminate the cycle by designating one constructor as the terminal point that does the actual initialization, and having all others delegate to it without calling each other.
public class Broken {
private int value;
public Broken(int value) {
this(value); // ERROR: recursive constructor invocation
}
public Broken() {
this(42); // Delegates to the above
}
}
3. Object Escaping this Before Initialization
The this-escape happens when a constructor hands out a reference to this before the object is fully initialized. The object exists in memory, but its invariants have not been established yet. Any code that receives this during construction can observe and interact with fields at their default values instead of the values the constructor was assigning. If that code calls back into the object, it operates on an inconsistent state.
The most common form is registering a listener or callback in the constructor and passing this as the argument. The listener receives the reference while the constructor is still running. If the listener fires synchronously during registration, it sees a partially constructed object. If it fires asynchronously, the object may be in a fully initialized state by the time it fires — but the window of vulnerability still existed during construction.
The inner class pattern in the example below illustrates the timing issue. The MyListener constructor receives this after name has been assigned, so the outer reference is safe in that specific case. But if name had not yet been assigned when registerListener was called, the listener would observe name as null. The defensive approach is to never pass this to another object until all fields are set and any setup logic has completed.
public class ThisEscape {
private final String name;
public ThisEscape(EventSource source) {
this.name = "initialized";
// DON'T: pass 'this' to another object before fields are set
source.registerListener(new MyListener(this)); // Listener might see uninitialized object
}
private class MyListener implements Listener {
private final ThisEscape outer;
public MyListener(ThisEscape outer) {
this.outer = outer; // At this point, name is already set — safe
}
}
}
Trade-off Table
| Constructor Type | Use Case | Limitation |
|---|---|---|
| Default (no-arg) | Simple classes, frameworks requiring empty constructor | Cannot enforce required fields |
| Parameterized | Mandatory fields must be set | Can get unwieldy with many parameters |
| Private | Singleton, factory pattern — control instantiation | Cannot subclass |
| Copy constructor | Create new instance from existing | Shallow copy unless explicitly deep |
| Chained (this()) | Reduce code duplication between constructors | Must be first statement |
Code Snippets
Constructor Chaining with this()
Constructor chaining via this() lets you define multiple initialization paths that all ultimately invoke the same base constructor. One constructor does the actual field assignments. The others delegate to it, each filling in defaults for some parameters. This means you write the initialization logic once, and every overload routes through it.
The constraint is that this() must be the first statement in the constructor body. This is not arbitrary syntax — it ensures the object is initialized in a predictable order. When you call this(url, headers, body), control transfers to the matching constructor, which runs its own this() call if it also chains further up, until the base constructor actually assigns the fields. No code in a delegating constructor runs after the this() call returns.
The HttpRequest example below shows a typical chain. The four-argument constructor is the base — it assigns all fields directly. Each shorter constructor chains upward, filling in a default value for the missing parameter. A caller who only has a URL gets a fully initialized object with a 30-second timeout, empty headers, and no body. The chain is never circular because each step moves toward more arguments, not back toward fewer.
public class HttpRequest {
private final String url;
private final int timeout;
private final Map<String, String> headers;
private final String body;
// Base constructor — does real work
public HttpRequest(String url, int timeout, Map<String, String> headers, String body) {
this.url = url;
this.timeout = timeout;
this.headers = headers;
this.body = body;
}
// Chain to base with default timeout
public HttpRequest(String url, Map<String, String> headers, String body) {
this(url, 30000, headers, body);
}
// Chain with default headers
public HttpRequest(String url, String body) {
this(url, 30000, Map.of(), body);
}
// Chain with minimum required
public HttpRequest(String url) {
this(url, 30000, Map.of(), null);
}
}
Copy Constructor
A copy constructor accepts an existing instance of the same class and produces a new, independent instance with the same field values. The pattern is new Player(existingPlayer) — explicit about intent, easy to read, and creates a clone without the ambiguity of clone() or serialization-based approaches. Copy constructors are useful when you need to pass a modified copy of an object into a method without letting the caller hold a reference that can mutate your internal state.
The key design decision is shallow versus deep copy. For immutable fields like String and primitives, the value is copied directly — both instances hold independent references to the same immutable value, which is fine. For mutable reference types like List<String>, you need to decide whether to copy the reference or copy the object itself. Storing the direct reference (this.inventory = inventory) means external code holding a reference to the original list can modify your internal state. The defensive copy approach (new ArrayList<>(inventory)) creates a new list with the same elements, so external changes cannot reach your internal state.
The Player example demonstrates this. The parameterized constructor takes ownership of a defensive copy of the inventory list. The copy constructor then delegates to the parameterized constructor, passing the fields from the source object. The result is a new Player that is functionally identical to the original but completely independent — mutations to the original’s inventory do not affect the copy.
public class Player {
private final String name;
private int health;
private final List<String> inventory;
public Player(String name, int health, List<String> inventory) {
this.name = name;
this.health = health;
this.inventory = new ArrayList<>(inventory); // Defensive copy
}
// Copy constructor
public Player(Player other) {
this(other.name, other.health, other.inventory);
}
}
Observability Checklist
- Required fields enforced via constructor parameters
- All constructor parameters validated before assignment
- Reference types defensively copied in constructor
-
this()orsuper()is first statement (or implicit) - No object escapes
thisbefore fully initialized
Security Notes
- Validate inputs — reject invalid values before assignment
- Defensive copies — copy mutable objects passed as parameters
- Immutable fields — use
finaland assign in constructor only - Don’t return
thisfrom constructor — enables partially constructed object access
public class SecureRequest {
private final List<String> allowedOrigins;
public SecureRequest(List<String> allowedOrigins) {
// Defensive copy — external list cannot affect our internal state
this.allowedOrigins = List.copyOf(allowedOrigins);
}
public List<String> getAllowedOrigins() {
return allowedOrigins; // Already immutable, safe to return
}
}
Pitfalls
- Forgetting
super()call — when parent has no default constructor, must explicitly call - Violating the “constructor should do minimal work” rule — heavy initialization in constructors hurts performance
- Creating objects in inconsistent state — don’t let object exist before invariants are established
- Too many constructor overloads — use Builder when parameter combinations become confusing
- Not defensive copying mutable parameters — storing direct references to mutable objects
// Bad: storing reference to mutable object
public class Cache {
private List<String> entries;
public Cache(List<String> entries) {
this.entries = entries; // External list can modify our data!
}
}
// Good: defensive copy
public class Cache {
private final List<String> entries;
public Cache(List<String> entries) {
this.entries = List.copyOf(entries); // Safe — we own our data
}
}
Quick Recap
- Default constructor = provided if no constructors defined, initializes fields to defaults
- Parameterized constructor = requires specific values at creation
- Constructor overloading = multiple constructors with different signatures
this(args)= chain to another constructor in same class (must be first line)super(args)= chain to parent constructor (must be first line, implicit if omitted)- Copy constructor =
new Player(existingPlayer)pattern for cloning
Interview Questions
Further Reading
- Fields and Instance Variables — understanding state initialization
- Classes and Objects — the new keyword and instantiation
- Inheritance in Java — super() and parent initialization
Conclusion
Constructors are the initialization gate through which every Java object passes at creation time. They ensure that objects enter the world in a valid state — setting required fields, validating parameters, and establishing invariants before any other code can interact with the object.
The distinction between default and parameterized constructors matters for API design: default constructors suit simple classes where all fields have sensible defaults, while parameterized constructors enforce that critical data is supplied at creation. Constructor overloading provides multiple initialization paths, with this() chaining reducing duplication by directing all paths to a single constructor that does the actual work.
The this() call for constructor chaining and super() call for parent initialization must be the first statement in a constructor. If omitted and the parent has a no-arg constructor, Java inserts an implicit super(); but if the parent only has parameterized constructors, you must call super(args) explicitly or compilation fails.
Security-conscious constructor design validates all inputs before assignment, makes defensive copies of mutable parameters, and uses final fields for anything that should not change after construction. This prevents invalid state from ever existing and blocks external code from corrupting internal state through stored references.
Constructors tie directly into the object lifecycle. After a constructor completes, the object is ready for use. If a constructor throws an exception, the object is never returned to the caller — this prevents partially constructed objects from escaping. This initialization protocol builds on the field initialization concepts covered in Fields and Instance Variables, and constructors themselves are invoked by the new keyword as detailed in Classes and Objects.
Category
Related Posts
Abstract Classes in Java
Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.
Arithmetic Operators in Java
Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.
Array Basics in Java
Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.