6.2
Constructors, Chaining, `this`
Constructors establish an object's starting state. Chaining keeps that work and its validation on one path.
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.
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 0The 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.
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.
| Constructor | Method | |
|---|---|---|
| Name | same as the class | any legal method name |
| Return type | none, not even void | required |
| Selected by | new, this(...), or super(...) | a method invocation |
| Purpose | initialise one new object | perform 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 28Under the hood
Going deeperDefault does not mean no-argument
These classes expose three different cases.
class A { }
class B {
B(String name) { }
}
class C {
C() { }
C(String name) { }
}| Class | Available constructors | |
|---|---|---|
| A | declares none | compiler declares default A() |
| B | declares B(String) | only B(String) |
| C | declares two | C() 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 newBefore 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.
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.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
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.
Why does `Student(String name) { name = name; }` leave the field null?
Show the answer
The parameter shadows the field. Both uses of plain
nameresolve 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.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 callthis(...)calls a superclass constructor withsuper(...), written explicitly or inserted by the compiler.
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
Lose the Default Constructor
ex-6-2-aFive 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
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
Hint 2
new B() stops compiling.Hint 3almost the answer
new Thing(), and nothing about your change looks like a removal.The Assignment That Does Nothing
ex-6-2-bRun 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
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
Hint 2
name = name are the parameter.Hint 3almost the answer
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.Chain Them Properly
ex-6-2-cThree 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
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
Hint 3almost the answer
What Runs First
ex-6-2-dSix 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
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
Hint 2
Hint 3almost the answer
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
- ConstructorFor an immutable object, the constructor is the only chance to set anything.8.2 - Immutable Classes
Constructor will return in 7.1 - Encapsulation, Inheritance, Packages, and `super`