Constructors and State
Bringing objects into existence in a valid state, overloaded constructors, and the role of `final` and immutability
A constructor's job is small but absolute: make sure that an object exists in a valid state from the moment it is born. A class that fails at this lets the rest of the system inherit half-built objects — and half-built objects produce bugs that look like ghosts.
What a constructor is
A constructor looks like a method but with two differences:
- It has the same name as the class.
- It has no return type.
When you write new Person("Ada", 30), Java allocates a new Person
on the heap and then calls the constructor on that fresh allocation
to initialize it.
The default constructor and what happens without one
If a class declares no constructors at all, Java secretly gives it a no-argument constructor that does nothing. The moment you declare any constructor, that freebie disappears.
Constructors enforce invariants from the very first moment
The thing constructors really do for design is enforce invariants
before the object is allowed to exist. The rule "a Person's age is
never negative" is a class rule — and the class can enforce it by
refusing to be constructed in violation of it.
This is enormously important. Once that constructor returns
successfully, every other method in Person can assume the name is
non-blank and the age is ≥ 0. Without that guarantee, every method
would need to start with defensive checks.
Overloaded constructors and this(...)
Java lets a class have several constructors with different parameter
lists. This is called constructor overloading. The most common
use is convenience: one "real" constructor that does the work, and a
shorter one that fills in defaults by calling the real one with
this(...).
The rule for this(...) chains: if you use it, it must be the first
statement in the constructor. This guarantees there is exactly one
"primary" constructor that does the actual initialization, and all
others funnel through it.
final fields and immutability
When a field is declared final, it must be assigned exactly once,
in the constructor (or at the declaration). After that it cannot be
reassigned.
An object whose fields are all final and whose constructor sets
them is immutable: it cannot change after creation. Immutable
objects are extremely safe — they can be passed around freely, shared
between methods, even shared between threads, with no risk of
accidental mutation.
That add method is a small pattern worth remembering: operations
return new instances instead of mutating the receiver. price
stays $19.99 forever; taxed is a new Money. This is how
strings, BigDecimal, LocalDate, and many other Java built-ins
work.
Multi-file practice: build a valid object
Implement User so it cannot be constructed in an invalid state.
Rules:
usernamemust be non-null and length>= 3. Otherwise throwIllegalArgumentException("invalid username").emailmust contain the character'@'. Otherwise throwIllegalArgumentException("invalid email").- Both fields are
private final. - Add public methods
username()andemail()that return the values.
Main runs three scenarios and is expected to print:
ok: ada @ ada@oop.dev
bad: invalid username
bad: invalid email
Test your understanding
What is the single most important job of a constructor?
To call all the other methods in the class
To bring an object into existence in a valid state, enforcing its invariants
To print a debug message describing the new object
To allocate memory for the object
If a class declares the constructor public Person(String name, int age) and no no-argument constructor, what happens when you write new Person()?
It runs Java's default no-arg constructor
It runs the two-argument constructor with null and 0
It fails to compile — there is no matching constructor
It produces a NullPointerException at runtime
Why is it useful for a class like Money to be immutable (all fields final, operations like add return new instances)?
Immutable objects are slightly larger in memory and that helps caching
The JVM can compile them down to machine code more easily
They cannot accidentally change underneath you, so they can be passed and shared freely without surprising mutations
They automatically run faster than mutable objects