Constructors

Su Suriya Ravichandran Updated 14 Nov 2026
10 min read ·Lesson 45 of 133

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:

ConstructorMethod
NameExactly the class nameAny valid identifier
Return typeNone, not even voidRequired (void or a type)
How it runsAutomatically, once, via newWhenever 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:

  1. Allocates memory for the object and sets every field to its default (null, 0, 0.0, false).
  2. Calls the parent class constructor (here Object()), via an implicit super().
  3. Runs field initializers and instance initializer blocks, in the order they appear in the class.
  4. Runs the rest of the constructor body.
  5. 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.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Constructors and get answers drawn from it.

Signed-in readers only.