Constructors
A constructor is a special block of code that runs when an object is created with new. Its job is to put the new object into a valid initial state: assigning required fields, validating inputs, and setting up anything the object needs before its methods can be used. Because the constructor runs before any other code can touch the object, it is the one place where an object's starting guarantees are established.
Declaring a Constructor
public class Student {
private final String name;
private final int rollNumber;
private double gpa;
public Student(String name, int rollNumber) {
this.name = name;
this.rollNumber = rollNumber;
this.gpa = 0.0;
}
}
Student s = new Student("Priya", 42);
A constructor differs from a method in three ways:
| Constructor | Method | |
|---|---|---|
| Name | Exactly the class name | Any valid identifier |
| Return type | None, not even void | Required (void or a type) |
| How it runs | Automatically, once, via new | Whenever it is called |
Adding a return type turns it into an ordinary method that happens to share the class's name:
public void Student(String name, int rollNumber) { ... } // a METHOD, not a constructor
This compiles, but new Student("Priya", 42) then fails, because no constructor accepts those arguments. IDEs warn about methods named like the class for this reason.
What Happens During new
For new Student("Priya", 42), the JVM:
- Allocates memory for the object and sets every field to its default (
null,0,0.0,false). - Calls the parent class constructor (here
Object()), via an implicitsuper(). - Runs field initializers and instance initializer blocks, in the order they appear in the class.
- Runs the rest of the constructor body.
- Returns a reference to the fully initialized object.
The order matters when a field has an initializer and the constructor also assigns it:
public class Order {
private String status = "NEW"; // step 3
private final long createdAt = System.currentTimeMillis();
public Order(boolean express) {
if (express) status = "PRIORITY"; // step 4: can override the initializer
}
}
Field initializers run first, so the constructor body can rely on them and can replace their values.
The Default Constructor
If a class declares no constructors, the compiler supplies one with no parameters and an empty body:
public class Settings {
private boolean darkMode;
private int fontSize = 14;
}
Settings s = new Settings(); // uses the compiler-generated default constructor
As soon as you declare any constructor, the default one is no longer generated:
public class Settings {
private int fontSize;
public Settings(int fontSize) { this.fontSize = fontSize; }
}
Settings s = new Settings(); // compile error: constructor Settings() not found
If both forms are needed, declare a no-argument constructor explicitly. Frameworks such as JPA and some JSON libraries require a no-argument constructor to create objects reflectively, which is a common reason to add one.
Parameterized Constructors and Required Data
A constructor's parameters express what an object must have to exist. If every Student needs a name and a roll number, requiring them in the constructor makes it impossible to create a student without them:
Student s = new Student(); // compile error: no such constructor
Student s = new Student("Priya", 42); // the only way to create one
Compare this with a no-argument constructor followed by setters:
Student s = new Student();
s.setName("Priya");
// forgot setRollNumber: the object exists in an incomplete state
Requiring data in the constructor moves the check from "hopefully the caller remembers" to "the compiler enforces it."
Validating in the Constructor
A constructor should reject invalid input, so an invalid object never exists:
public class Student {
private final String name;
private final int rollNumber;
public Student(String name, int rollNumber) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name is required");
}
if (rollNumber <= 0) {
throw new IllegalArgumentException("rollNumber must be positive: " + rollNumber);
}
this.name = name.strip();
this.rollNumber = rollNumber;
}
}
If the constructor throws, new throws, and no reference to the half-built object is ever returned. Every method in the class can then assume name is non-blank and rollNumber is positive, without re-checking.
final Fields and Constructors
A final field must be assigned exactly once: either where it is declared or in every constructor.
public class Invoice {
private final String id;
private final LocalDate issued;
public Invoice(String id) {
this.id = id;
this.issued = LocalDate.now();
}
}
The compiler checks this. Forgetting to assign a final field on any path, or assigning it twice, is a compile error. final fields make an object's identity and core data impossible to change accidentally later.
Multiple Constructors
A class can offer several constructors with different parameter lists. One constructor can call another with this(...):
public class Student {
private final String name;
private final int rollNumber;
private final String department;
public Student(String name, int rollNumber, String department) {
this.name = name;
this.rollNumber = rollNumber;
this.department = department;
}
public Student(String name, int rollNumber) {
this(name, rollNumber, "General"); // delegate to the full constructor
}
}
Validation and assignment then live in one constructor, and the others supply defaults. Constructor overloading and this(...) are covered in the next lessons.
Constructors and Inheritance
A subclass constructor always runs a parent constructor before initializing its own fields. If you do not write a call, the compiler inserts super(), a call to the parent's no-argument constructor:
public class Person {
protected final String name;
public Person(String name) { this.name = name; }
}
public class Teacher extends Person {
private final String subject;
public Teacher(String name, String subject) {
super(name); // must call Person(String): Person has no no-arg constructor
this.subject = subject;
}
}
Without the explicit super(name), the compiler would insert super(), find no matching constructor in Person, and report an error. Parent state is always initialized before child state. The super keyword has its own lesson.
Version note: code before super(...) and this(...)
Up to Java 24, an explicit super(...) or this(...) call had to be the first statement in a constructor. Java 25 finalized flexible constructor bodies: statements may now appear before the call, as long as they do not use the object being constructed (no reading its fields or calling its instance methods). This allows validating or preparing arguments first:
public Teacher(String name, String subject) {
if (subject.isBlank()) throw new IllegalArgumentException("subject required"); // Java 25+
super(name);
this.subject = subject;
}
On earlier versions, move such checks into a static helper method called inside the super(...) argument list, or perform them after the call.
Constructors are not inherited. Teacher does not automatically get a Teacher(String name) constructor just because Person has one.
Copy Constructors
A copy constructor creates a new object with the same state as an existing one:
public class Playlist {
private final String name;
private final List<String> songs;
public Playlist(String name) {
this.name = name;
this.songs = new ArrayList<>();
}
public Playlist(Playlist other) {
this.name = other.name + " (copy)";
this.songs = new ArrayList<>(other.songs); // copy the list, not the reference
}
public void add(String song) { songs.add(song); }
public int size() { return songs.size(); }
}
Playlist original = new Playlist("Focus");
original.add("Track 1");
Playlist copy = new Playlist(original);
copy.add("Track 2");
System.out.println(original.size()); // 1
System.out.println(copy.size()); // 2
new ArrayList<>(other.songs) creates a separate list. Writing this.songs = other.songs would make both playlists share one list, so adding to the copy would also change the original. This is the difference between a deep and a shallow copy of mutable fields.
The same care applies whenever a constructor receives a mutable object such as a list or array: store a copy if the object must not change behind the class's back.
Private Constructors
Making a constructor private prevents other classes from calling new directly. This is used to:
- Force creation through static factory methods, which can validate, cache, or choose a subclass:
public class Money {
private final long paise;
private Money(long paise) { this.paise = paise; }
public static Money ofRupees(double rupees) {
return new Money(Math.round(rupees * 100));
}
}
- Prevent instantiation of utility classes that contain only static methods:
public final class StringUtils {
private StringUtils() {} // no instances
public static boolean isBlank(String s) { return s == null || s.isBlank(); }
}
Things a Constructor Should Not Do
Long-running or risky work. Opening network connections, reading large files, or starting threads in a constructor makes objects slow and failure-prone to create, and hard to test. Accept already-prepared dependencies as parameters, or provide a separate start() or load() method.
Calling overridable methods. A constructor that calls a method a subclass overrides will run the subclass version before the subclass's own fields are initialized:
class Base {
Base() { describe(); } // calls the overridden version
void describe() {}
}
class Child extends Base {
private String label = "child";
@Override void describe() { System.out.println(label.toUpperCase()); }
}
new Child(); // NullPointerException: label is still null when describe() runs
Base() runs before Child's field initializers, so label is still null. Call only private or final methods from constructors.
Leaking this. Passing this to other code (registering a listener, adding to a static list) before the constructor finishes lets that code see an incompletely built object.
Record Constructors
Records generate a canonical constructor that takes every component. A compact constructor adds validation without repeating the assignments:
public record Range(int min, int max) {
public Range { // compact constructor
if (min > max) {
throw new IllegalArgumentException(min + " > " + max);
}
}
}
new Range(1, 10); // fine
new Range(10, 1); // IllegalArgumentException
The fields are assigned automatically after the compact constructor's body runs. Records have their own lesson.
Practical Example: Course Enrollment
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
public class Course {
private static final int DEFAULT_CAPACITY = 30;
private final String code;
private final String title;
private final int capacity;
private final LocalDate startDate;
private final List<String> enrolled = new ArrayList<>();
public Course(String code, String title, int capacity, LocalDate startDate) {
if (code == null || !code.matches("[A-Z]{2,4}\\d{3}")) {
throw new IllegalArgumentException("Invalid course code: " + code);
}
if (capacity < 1 || capacity > 500) {
throw new IllegalArgumentException("Capacity out of range: " + capacity);
}
this.code = code;
this.title = title.strip();
this.capacity = capacity;
this.startDate = startDate;
}
public Course(String code, String title) {
this(code, title, DEFAULT_CAPACITY, LocalDate.now().plusWeeks(2));
}
public boolean enroll(String student) {
if (enrolled.size() >= capacity || enrolled.contains(student)) {
return false;
}
return enrolled.add(student);
}
@Override
public String toString() {
return code + " " + title + " (" + enrolled.size() + "/" + capacity
+ ", starts " + startDate + ")";
}
public static void main(String[] args) {
Course java = new Course("CS101", "Java Basics", 2, LocalDate.of(2026, 12, 1));
java.enroll("Ada");
java.enroll("Linus");
System.out.println(java.enroll("Grace")); // false: full
System.out.println(java);
Course docker = new Course("DEV210", " Docker Essentials ");
System.out.println(docker);
try {
new Course("java-1", "Bad code");
} catch (IllegalArgumentException e) {
System.out.println("Rejected: " + e.getMessage());
}
}
}
Example output (the Docker start date depends on the current date):
false
CS101 Java Basics (2/2, starts 2026-12-01)
DEV210 Docker Essentials (0/30, starts 2026-11-24)
Rejected: Invalid course code: java-1
The full constructor validates the code format and capacity, trims the title, and assigns every final field. The two-argument constructor delegates to it with defaults, so validation is never duplicated. The enrolled list is initialized at its declaration, so it is ready before either constructor body runs. An invalid code is rejected at construction time, which means every Course object that exists anywhere in the program is guaranteed to be valid.