Layers of Logic

6.2

Constructors, Chaining, `this`

Constructors establish an object's starting state. Chaining keeps that work and its validation on one path.

Core20 min read4 exercises
01

Previously on

Section 6.1 built each Student in several writes.

Student aditya = new Student();
aditya.name = "Aditya";
aditya.age = 28;
aditya.rollNumber = 101;

new Student() already invoked a constructor. The compiler supplied one because the class declared none.

02

The problem

An object can escape before those later assignments finish.

Student s = new Student();
s.name = "Aditya";
register(s);                 // age and rollNumber are still 0

The compiler sees a valid Student reference. It cannot know which default field values mean “not ready” in your design.

Validation has no single home either. Ten call sites can apply ten different checks before assigning an age.

The type should require its starting data at creation time. Invalid input should fail at that boundary.

03

The idea

A constructor is special initialisation code selected by a class instance creation expression.

class Student {
    String name;
    int age;
    int rollNumber;

    Student(String name, int age, int rollNumber) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("name is required");
        }
        if (age < 0) {
            throw new IllegalArgumentException("age cannot be negative");
        }

        this.name = name;
        this.age = age;
        this.rollNumber = rollNumber;
    }
}
Student aditya = new Student("Aditya", 28, 101);

There is no zero-argument route into this class. Every caller must provide all three values.

ConstructorMethod
Namesame as the classany legal method name
Return typenone, not even voidrequired
Selected bynew, this(...), or super(...)a method invocation
Purposeinitialise one new objectperform an operation

Adding a return type changes the declaration into a method.

void Student(String name) {        // method, not constructor
    this.name = name;
}

new Student() will never call that method.

A complete constructor trace

class Student {
    String name;
    int age;

    Student(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("built " + name);
    }

    String label() {
        return name + " is " + age;
    }
}

public class Demo {
    public static void main(String[] args) {
        Student s = new Student("Aditya", 28);
        System.out.println(s.label());
    }
}
built Aditya
Aditya is 28
04

Under the hood

Going deeper

Default does not mean no-argument

These classes expose three different cases.

class A { }

class B {
    B(String name) { }
}

class C {
    C() { }
    C(String name) { }
}
ClassAvailable constructors
Adeclares nonecompiler declares default A()
Bdeclares B(String)only B(String)
Cdeclares twoC() and C(String)

The compiler’s default constructor has the same access as the class. Its first action invokes the superclass constructor with no arguments.

If that superclass call is not legal, compilation fails. The compiler does not invent arguments for a superclass constructor.

this selects the current object

Constructor parameters often use the field names they will initialise.

Student(String name, int age) {
    this.name = name;
    this.age = age;
}

The parameter shadows the field inside the constructor body. this.name selects the field on the current object.

this may also pass the current object to another method. Do not let it escape from a constructor unless the receiving code accepts a partly initialised object.

Overloading and this(...)

Constructors overload by parameter types, following the method rules from Section 5.1.

class Student {
    String name;
    int age;
    int rollNumber;
    String college;

    Student(String name, int age, int rollNumber, String college) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("name is required");
        }
        this.name = name;
        this.age = age;
        this.rollNumber = rollNumber;
        this.college = college;
    }

    Student(String name, int rollNumber) {
        this(name, 0, rollNumber, "Unknown");
    }

    Student(String name) {
        this(name, 0);
    }
}

All routes reach the four-argument constructor. The validation and field assignments occur once in source code.

this(...) must be the first statement. It selects another constructor in the same class.

A chain cannot loop back to itself. The compiler rejects recursive constructor invocation.

this(...) and super(...) occupy the same slot

Every constructor begins by invoking another constructor.

  • this(...) selects one in the same class.
  • super(...) selects one in the direct superclass.
  • If neither is written, the compiler inserts super().

Both forms must be first, so one constructor cannot write both. A this(...) chain eventually reaches a constructor that invokes super(...).

The initialisation order

Consider this runnable trace.

class Person {
    String kind = report("2. Person field");

    Person() {
        report("3. Person constructor");
    }

    static String report(String text) {
        System.out.println(text);
        return text;
    }
}

class Student extends Person {
    String name = report("4. Student field");

    { report("5. Student block"); }

    Student() {
        report("6. Student constructor");
    }
}

public class Demo {
    public static void main(String[] args) {
        System.out.println("1. before new");
        new Student();
        System.out.println("7. after new");
    }
}
1. before new
2. Person field
3. Person constructor
4. Student field
5. Student block
6. Student constructor
7. after new

Before this trace, the JVM writes default values to all instance fields. The constructor chain then reaches the superclass first.

For each class, field initialisers and instance blocks run in source order. That class’s constructor body runs after them.

Static initialisation is a separate class-level event. It happens before the first active use that needs the class initialised, as Section 6.4 explains.

05

What it costs

Long constructor parameter lists are hard to read and easy to swap. Several parameters of the same type make this worse.

Many overloads can also hide which defaults a caller receives. Named static factories or builders become clearer as the choices grow.

Constructor validation can throw. That cost keeps invalid state from travelling deeper into the program.

A public no-argument constructor may be required by a framework. It also allows an object with only default field values, so choose it with intent.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. What is the difference between a default constructor and a no-argument constructor?

    Show the answer

    A no-argument constructor is any constructor whose parameter list is empty. You may write one yourself.

    The default constructor is the constructor the compiler declares only when the class declares no constructor. It has no parameters and calls the superclass constructor.

    After you declare any constructor, the compiler does not add a default constructor. Write your own no-argument constructor if the class still needs one.

  2. Why does `Student(String name) { name = name; }` leave the field null?

    Show the answer

    The parameter shadows the field. Both uses of plain name resolve to the parameter, so the assignment writes its value back to itself.

    this.name = name; selects the current object's field on the left and the parameter on the right.

  3. A constructor begins with `this(...)`. When does its own body run?

    Show the answer

    The selected constructor runs first. If it chains again, that next constructor runs first too. Bodies return through the chain in reverse order.

    this(...) must be the first statement. A constructor that does not call this(...) calls a superclass constructor with super(...), written explicitly or inserted by the compiler.

07

Exercises

Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.

4 exercises85 pointsabout 90 minutes

The Layers of Logic VS Code extension runs the checks for exercises marked checked. For a manual exercise, run the program and compare its behaviour with the stated requirements and sample output.
A

Lose the Default Constructor

Warm up·15 min·15 points

checkedex-6-2-a

Five lines. Some compile, one does not. Predict all five first.

The failing one is interesting because nothing about it looks wrong. It stops compiling because of something added somewhere else in the file.

Read the error message carefully and write down the rule in your own words. It is one sentence, and it is about the class as a whole rather than about any particular constructor.

Then answer the design question. You could make the failing line work by adding an empty constructor back. Should you? Say why or why not.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Defaults {
    static boolean canCallNoArg(String which)
    static String defaultNameOfA()
    static String nameOfC()
    static String nameOfCWith(String n)
    static int constructorCount(String which)
}

A, B and C are nested classes exactly as described. canCallNoArg and constructorCount take "A", "B" or "C" and record what you found by trying it.

What your program must do

  • Try the no argument constructor on all three and record which work
  • Say how many constructors A really has
  • Explain what writing a constructor does to the free one
  • Say why this matters when you add a constructor to a class other people use
Defaults.java
public class Defaults {

    static class A { String name; }                                     // no constructor at all
    static class B { String name; B(String name) { this.name = name; } }
    static class C {
        String name;
        C() { this.name = "default"; }
        C(String name) { this.name = name; }
    }

    // Try new A(), new B() and new C() for yourself, then record what happened.
    static boolean canCallNoArg(String which) { return true; }  // TODO

    static String defaultNameOfA()      { return ""; }  // TODO
    static String nameOfC()             { return ""; }  // TODO
    static String nameOfCWith(String n) { return ""; }  // TODO

    // How many constructors does each class actually have?
    static int constructorCount(String which) { return 0; }  // TODO

    public static void main(String[] args) {
        // TODO: try new B() and read the error before recording anything
    }
}
Hint 1
A has a constructor. You did not write it and it exists, and it sets nothing, which is why the field keeps its default.
Hint 2
The free one appears only when you write none at all. Add any constructor and it is gone, and new B() stops compiling.
Hint 3almost the answer
That is a source compatibility trap. Adding a constructor to a released class breaks every caller that wrote new Thing(), and nothing about your change looks like a removal.
What this is really testing

Whether you can predict when Java gives you a free constructor and when it stops. This causes a compile error in code you did not touch, which is confusing until you know the rule.

B

The Assignment That Does Nothing

Real work·20 min·20 points

checkedex-6-2-b

Run this before you read the code carefully. The output will tell you something is wrong, and the constructor looks completely correct.

Two assignments that do nothing at all, with no warning from anywhere.

Explain what is happening, using the right word for it. Then fix it two different ways: once with this, once by changing a name.

Both work. Say which one you would use in real code and why. There is a widely followed convention here, and the reason for it is about readability rather than correctness.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Shadow {
    static String brokenName()
    static int brokenReadiness()
    static String fixedName()
    static int fixedReadiness()
    static String renamedName()
    static boolean compilerWarns()
}

Three nested classes: BrokenUnit keeps the bug exactly as written, ThisUnit fixes it with this, RenamedUnit fixes it by renaming the parameters. All three are constructed with Atlas and 88.

What your program must do

  • Run the broken version first and record what it prints
  • Explain why the assignments do nothing, using the word shadowing
  • Fix it with this, and confirm the output changes
  • Fix it a second way without this, and say which you prefer
Shadow.java
public class Shadow {

    // Leave this one broken. Run it BEFORE fixing anything.
    static class BrokenUnit {
        String name;
        int readiness;
        BrokenUnit(String name, int readiness) {
            name = name;              // bug
            readiness = readiness;    // bug
        }
    }

    // TODO: the same class fixed with this
    static class ThisUnit {
        String name;
        int readiness;
        ThisUnit(String name, int readiness) { }
    }

    // TODO: fixed a second way, WITHOUT using this
    static class RenamedUnit {
        String name;
        int readiness;
        RenamedUnit(String name, int readiness) { }
    }

    static String brokenName()   { return ""; }  // TODO
    static int brokenReadiness() { return -1; }  // TODO
    static String fixedName()    { return ""; }  // TODO
    static int fixedReadiness()  { return -1; }  // TODO
    static String renamedName()  { return ""; }  // TODO

    // Did anything warn you about the broken version?
    static boolean compilerWarns() { return true; }  // TODO

    public static void main(String[] args) {
        // TODO: run the broken one first and record what prints
    }
}
Hint 1
Run it before fixing anything. Both fields are empty, which is surprising given that the constructor clearly assigns them.
Hint 2
Inside a method, the nearest declaration with that name wins. The parameter is nearer than the field, so both sides of name = name are the parameter.
Hint 3almost the answer
The second fix is to rename the parameters, for example Unit(String unitName, int unitReadiness). It works, and almost nobody does it, because matching the names is the convention everywhere and it makes the constructor read as documentation.
What this is really testing

Whether you can spot shadowing. This bug compiles, runs, produces no warning, and leaves your fields empty, which makes it one of the most confusing bugs a beginner can hit.

C

Chain Them Properly

Real work·30 min·25 points·The Registry

checkedex-6-2-c

Three constructors. Only one of them is allowed to assign a field.

Put every assignment and every validation rule in the constructor with the most parameters. The other two should be one line each, chaining to it with defaults filled in.

Then do the part that proves it worked. Add a fourth rule after everything is written, such as rejecting an id below 100.

If you only had to edit one method, your chaining is correct. If you had to edit three, go back and fix it, because you have just watched the exact bug this pattern exists to prevent: a rule that lives in three places and gets added to two of them.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Chained {
    static String describe(String name, int id, double readiness, boolean active)
    static String describe(String name, int id)
    static String describe(String name)
    static String rejects(String name, int id, double readiness)
    static int placesValidationLives()
}

Unit is a nested class with three constructors. The full one does all the work and all the validation. The other two chain to it with this(...) and contain no assignments at all. toString gives name/id/readiness/active. rejects returns "accepted" or the rejection message.

What your program must do

  • Write one full constructor holding all the work and all the validation
  • Chain the two shorter ones to it, with no copied assignments
  • Show that the short constructors are validated too
  • Say what goes wrong if you copy the validation into each constructor instead
Chained.java
public class Chained {

    static class Unit {
        String name;
        int id;
        double readiness;
        boolean active;

        // TODO: one FULL constructor that does all the work and all the validation:
        //   name must not be null or blank
        //   id must not be negative
        //   readiness must be 0 to 100
        //   reject with IllegalArgumentException, naming the field

        // TODO: Unit(String name, int id)  -> readiness 0.0, active true
        // TODO: Unit(String name)          -> id 0 as well
        // Each short one chains with this(...). No copied assignments anywhere.

        @Override public String toString() {
            return name + "/" + id + "/" + readiness + "/" + active;
        }
    }

    static String describe(String name, int id, double readiness, boolean active) { return ""; }  // TODO
    static String describe(String name, int id) { return ""; }  // TODO
    static String describe(String name)         { return ""; }  // TODO

    // "accepted", or the rejection message.
    static String rejects(String name, int id, double readiness) { return ""; }  // TODO

    // How many places do the validation rules appear in your code?
    static int placesValidationLives() { return 3; }  // TODO

    public static void main(String[] args) {
        // TODO: build a unit three ways and show the defaults filled in
    }
}
Hint 1
this(...) must be the very first statement in a constructor. Nothing can come before it, not even a validation check, which is part of why the full one does all of it.
Hint 2
The short constructors should contain exactly one line each. If yours have assignments in them, they are not chaining.
Hint 3almost the answer
Copying the validation means three copies of one rule. Change the readiness limit and you have to find all three, and the one you miss is the constructor nobody uses until the day they do.
What this is really testing

Whether you can put validation in exactly one place. Three constructors with copied setup is three places a rule can be forgotten, and this exercise makes you feel that by adding a rule afterwards.

D

What Runs First

Hard·25 min·25 points

checkedex-6-2-d

Six numbered things, printing as they run. Predict the exact output before running anything.

There are two sequences mixed together here, and separating them is the exercise. One sequence happens once for the whole class. The other happens on every new. Work out which of the six belongs to which before you look.

Then extend it. Add a field that has both an initialiser and a constructor assignment, and predict which value survives.

Finish by answering the practical question. If a constructor reads a field before assigning it, what does it see? The ordering you just worked out gives you the answer, and this is a real bug shape rather than a curiosity.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Order {
    static int report(String what)
    static List<String> firstObject()
    static List<String> secondObject()
    static List<String> viaChainedConstructor()
    static boolean staticsRunPerObject()
}

Noisy is a nested class with a static field initialiser, a static block, an instance field initialiser, an instance block and two constructors, each calling report with its number. report records into a shared list so the checks can read the order. Each of the three methods clears the log at the right moment and returns what happened.

What your program must do

  • Predict the full order for the first object before running
  • Show that the static parts do not run for the second object
  • Say where the constructor body sits in the order
  • Show what this() does to the order
Order.java
import java.util.*;

public class Order {

    static final List<String> log = new ArrayList<>();

    static class Noisy {
        static int staticCounter = report("1. static field initialiser");
        static { report("2. static block"); }

        int instanceField = report("3. instance field initialiser");
        { report("4. instance block"); }

        Noisy() { report("5. constructor body"); }

        Noisy(String tag) {
            this();
            report("6. second constructor, after this()");
        }
    }

    static int report(String what) { return 0; }  // TODO: record it

    // PREDICT each of these before running.
    static List<String> firstObject()          { return List.of(); }  // TODO
    static List<String> secondObject()         { return List.of(); }  // TODO
    static List<String> viaChainedConstructor(){ return List.of(); }  // TODO

    static boolean staticsRunPerObject() { return true; }  // TODO

    public static void main(String[] args) {
        // TODO: write down your predicted order, then run all three
    }
}
Hint 1
Creating the first Noisy object is an active use. The JVM initializes Noisy before constructing that object, so its static field initializer and static block run first.
Hint 2
Clear the log at the right moment. For the second object you have to make one first, or you will catch the static work as well.
Hint 3almost the answer
The constructor body is LAST, after every field initialiser and instance block. That is why a field initialiser cannot depend on something the constructor sets.
What this is really testing

Whether you can separate one-time class initialization from the field, block, and constructor steps repeated for each new object.

08

After the credits

Section 7.1 expands super(...) into inheritance and constructor chaining across a class hierarchy.

Section 8.2 makes construction the only write path for immutable fields. Validation then becomes part of the type’s long-term guarantee.

A constructor may still store a caller’s mutable reference. The caller can change that object later. Section 6.3 shows why defensive copies close that route.

Threads you opened in this section