Why Constructors Exist
In the previous lesson you created an object and then assigned its fields one line at a time. That works, and it has two problems. The first is that the object exists in a half-built state in between — a Student with a name but no roll number is not a valid student, and if anything reads it during those few lines you get nonsense. The second is that nothing forces the caller to fill in every field. Forget one line and you have an object with a null name that will fail somewhere else entirely, far from the mistake.
A constructor fixes both. It is a special block that runs automatically when new creates an object, and it can demand the values it needs as parameters. Now the only way to create a Student is to supply a name, a roll number and marks — the compiler enforces it — and the object is complete and valid from the instant it exists.
A constructor is recognised by two rules: its name is exactly the class name, and it has no return type at all, not even void. That second rule is not a style preference; it is how Java tells a constructor from a method. Write public void Student(...) and you have quietly declared an ordinary method that happens to be named Student. It compiles, it never runs when you call new, and your fields stay null. This is one of the hardest beginner bugs to spot because everything looks right.
Beyond assigning fields, a constructor is the right place to check that the values make sense. Reject a negative roll number or marks above 100 here, by throwing an exception, and no invalid object can ever exist in your program. Validating later, at the point of use, means writing the same check in ten places and forgetting it in the eleventh.
public class Student {
private final String name;
private final int rollNo;
private double marks;
// Constructor: class name, NO return type
public Student(String name, int rollNo, double marks) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name is required");
}
if (marks < 0 || marks > 100) {
throw new IllegalArgumentException("Marks must be 0-100, got " + marks);
}
this.name = name;
this.rollNo = rollNo;
this.marks = marks;
}
// NOT a constructor — note the 'void'. This is just a method.
// public void Student(String name) { this.name = name; }
@Override
public String toString() {
return rollNo + " " + name + " " + marks;
}
}
Student s = new Student("Ananya", 101, 87.5); // complete and valid
System.out.println(s); // 101 Ananya 87.5
// new Student("Rahul", 102, 150);
// IllegalArgumentException: Marks must be 0-100, got 150.0
// new Student("Meera"); // compile error — no such constructor - If your class has fields that should never change after creation, mark them
final. Afinalfield must be assigned exactly once, and the constructor is the only place it can happen — so the compiler will tell you if a constructor forgets one.
The Default Constructor, and the Moment It Disappears
In the previous lesson you wrote new Student() for a class that had no constructor at all, and it worked. That is because when a class declares no constructors whatsoever, the compiler quietly inserts an empty, no-argument one for you. It is called the default constructor, and it does nothing beyond leaving the fields at their defaults.
The rule people get caught by is what happens next. The moment you write any constructor of your own, the compiler stops providing the default. So adding a three-argument constructor to a class that other code was creating with new Student() breaks every one of those calls with constructor Student in class Student cannot be applied to given types. Nothing is wrong with your new constructor; the free one simply went away.
If you still want a no-argument version, write it explicitly. That is a deliberate decision rather than an accident: a no-argument constructor is worth having when there are sensible defaults for every field, and worth omitting when there are not, because omitting it is how you force callers to supply the required data.
Note also that the default constructor is public only if the class is public. And constructors are not inherited — a subclass does not get its parent's constructors and must declare its own.
// Class A declares no constructor -> compiler supplies Student()
class Book {
String title;
double price;
}
Book b = new Book(); // works
// Class B declares one -> the free default disappears
class Magazine {
String title;
int issue;
public Magazine(String title, int issue) {
this.title = title;
this.issue = issue;
}
}
// Magazine m = new Magazine();
// error: constructor Magazine in class Magazine cannot be applied
// to given types; required: String,int found: no arguments
// If you want both, declare both
class Journal {
String title = "Untitled";
int issue = 1;
public Journal() { } // explicit no-arg
public Journal(String title, int issue) {
this.title = title;
this.issue = issue;
}
}
Journal j1 = new Journal();
Journal j2 = new Journal("Physics Today", 42); - Several frameworks — JPA/Hibernate, Jackson for JSON, and older Java Beans tooling — create objects reflectively and require a no-argument constructor to exist. If a library reports that it cannot instantiate your class, a missing no-arg constructor is the usual cause.
Overloading Constructors and Chaining with this()
Constructors overload exactly like methods: several of them, same name, different parameter lists. This is how a class offers a choice — create a User with just a name, or with a name and an email, or with everything.
Written naively, each of those constructors repeats the same assignments and, worse, the same validation. When someone later adds a check for the email format, they will add it to one constructor and not the others, and objects created through the forgotten path will be invalid. This is a real maintenance bug, not a theoretical one.
Constructor chaining is the fix. this(...) inside a constructor calls another constructor of the same class. Put every piece of real work in one constructor — the most complete one — and have the shorter ones fill in defaults and delegate to it. Now validation exists in exactly one place.
Two rules govern this(...). It must be the very first statement in the constructor; nothing may come before it. And a constructor cannot call itself directly or in a cycle — the compiler detects that and reports recursive constructor invocation.
When to use which: chained constructors are ideal up to about three or four parameters. Beyond that, a call like new Order(1, 2, true, false, 3, null) is unreadable and easy to get wrong by transposing two arguments of the same type — a bug the compiler cannot catch. At that size, use a builder or pass a small configuration object instead.
public class User {
private final String name;
private final String email;
private final int age;
// The ONE constructor that does real work
public User(String name, String email, int age) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name is required");
}
if (age < 0 || age > 130) {
throw new IllegalArgumentException("Unrealistic age: " + age);
}
this.name = name;
this.email = email;
this.age = age;
}
// Shorter versions delegate — validation is never duplicated
public User(String name, String email) {
this(name, email, 0); // MUST be the first statement
}
public User(String name) {
this(name, "", 0);
}
@Override
public String toString() {
return name + " <" + email + "> age " + age;
}
}
User u1 = new User("Ananya", "ananya@example.com", 22);
User u2 = new User("Rahul", "rahul@example.com");
User u3 = new User("Meera");
// this(...) must come first:
// public User(String name) {
// System.out.println("creating"); // error: call to this() must be
// this(name, "", 0); // first statement in constructor
// } - Reading a chain from the outside in tells you the design: the constructor that does not call
this(...)is the real one, and everything else is a convenience wrapper. Follow that convention and other developers will find the logic immediately.
Constructors and Inheritance: super()
When a class extends another, creating a child object must also initialise the parent part of it. Java handles this by requiring that a parent constructor runs first, before the child constructor body. You call it with super(...), and like this(...) it must be the first statement.
If you do not write super(...) yourself, the compiler inserts a call to the parent's no-argument constructor. That is invisible and works fine — right up until the parent has only constructors that take arguments. Then you get constructor Animal in class Animal cannot be applied to given types pointing at your child constructor, which is baffling until you know the compiler put a hidden super() there. The fix is to call the correct parent constructor explicitly.
A constructor may contain this(...) or super(...), but not both, since each must be the first statement. That is not a limitation in practice: the constructor you chain to with this(...) will itself call super(...).
The full order of events when you write new Dog(...) is worth knowing once. The parent constructor runs first, all the way up the chain to Object. Then the child's field initialisers run, in the order they appear in the source. Then the child's constructor body runs. Inheritance itself is covered properly two lessons from now — the point here is simply that constructors are the mechanism that makes it work.
class Animal {
protected final String name;
public Animal(String name) { // no no-arg constructor here
this.name = name;
System.out.println("Animal constructor");
}
}
class Dog extends Animal {
private final String breed;
public Dog(String name, String breed) {
super(name); // MUST come first, and MUST be explicit
this.breed = breed; // because Animal has no Animal()
System.out.println("Dog constructor");
}
}
new Dog("Rex", "Labrador");
// Animal constructor
// Dog constructor <- parent always finishes first
// Leaving super(name) out:
// class Dog extends Animal {
// public Dog(String breed) { this.breed = breed; }
// }
// error: constructor Animal in class Animal cannot be applied to given types;
// required: String found: no arguments
// (the compiler inserted an invisible super() ) - Avoid calling an overridable method from inside a constructor. The parent constructor runs before the child's fields are initialised, so if the parent constructor calls a method the child has overridden, that method runs while the child's fields are still null or zero. It is a subtle bug and a common interview question.
Private Constructors, Static Factories and Records
A constructor does not have to be public. Making it private means no code outside the class can call new on it, which sounds useless until you see what it enables.
The first use is a static factory method: a public static method that creates and returns the object, with the constructor kept private so it is the only route in. A factory has advantages a constructor cannot offer — it has a meaningful name, so Duration.ofMinutes(5) reads better than a constructor taking a bare number; it can return a cached object instead of always creating a new one; and two factories with the same parameter types can coexist, which overloaded constructors cannot. You have already used several without noticing: List.of(...), Integer.valueOf(...) and String.valueOf(...) are all static factories.
The second use is a class that should never be instantiated at all — a utility class holding only static methods, like Math or Arrays. A private constructor documents and enforces that.
Finally, if the class is nothing but data, a record (Java 16+) generates the constructor for you. Its compact constructor form lets you add validation without repeating the assignments — you write only the checks, and the compiler adds the field assignments afterwards.
// Static factory with a private constructor
public class Temperature {
private final double celsius;
private Temperature(double celsius) { // nobody outside can call new
this.celsius = celsius;
}
public static Temperature ofCelsius(double c) {
return new Temperature(c);
}
public static Temperature ofFahrenheit(double f) {
return new Temperature((f - 32) * 5 / 9);
}
// Two constructors both taking one double would be impossible.
}
Temperature t1 = Temperature.ofCelsius(37.0);
Temperature t2 = Temperature.ofFahrenheit(98.6);
// A utility class that must never be instantiated
public final class MathUtils {
private MathUtils() { } // prevents new MathUtils()
public static int square(int n) { return n * n; }
}
// A record: constructor, accessors, equals, hashCode, toString all generated
public record Marks(String subject, int score) {
// compact constructor — validation only, assignments are added for you
public Marks {
if (score < 0 || score > 100) {
throw new IllegalArgumentException("score must be 0-100");
}
}
}
Marks m = new Marks("Physics", 88);
System.out.println(m); // Marks[subject=Physics, score=88]
System.out.println(m.score()); // 88 - Same name as the class, and no return type — not even
void - You get a free no-argument constructor only while you declare none of your own
this(...)chains to another constructor of the same class;super(...)chains to the parent- Either one must be the first statement, and you cannot use both in one constructor
- Constructors are not inherited
- Validate here so that an invalid object can never exist anywhere in your program
- Once a constructor takes more than about four parameters, especially several of the same type, consider a builder.
new Order(1, 2, true, false)tells a reader nothing, and swapping two of those arguments compiles perfectly while producing a completely different order.
