Skip to content

VOL 01 / CH 07 / LESSON 03

7.3 Constructors and Object Invariants ​

Prerequisites: Classes, Fields, Methods, and this

Estimated time: 30 minutes

You are ready to continue when: You can construct valid objects and explain default constructors, overloading, chaining, and initialization order

1. Constructors Share the Class Name and Have No Return Type ​

java
import java.util.Objects;

class Book {
    private final String title;
    private final String author;
    private final double price;

    Book(String title, String author, double price) {
        this.title = requireText(title, "title");
        this.author = requireText(author, "author");
        if (!Double.isFinite(price) || price < 0.0) {
            throw new IllegalArgumentException("invalid price: " + price);
        }
        this.price = price;
    }

    private static String requireText(String value, String name) {
        Objects.requireNonNull(value, name);
        if (value.isBlank()) {
            throw new IllegalArgumentException("blank " + name);
        }
        return value;
    }
}

Constructors are not ordinary methods with special names: they have no return type, cannot be called again on an object, and are not inherited. new Book(...) selects an accessible constructor with matching parameters.

final instance fields must be assigned once on every normal completion path of construction. After that, fields cannot point to other values, which is suitable for object identity information and immutable state.

The example using double is for linking primitive types, but binary floating points cannot precisely represent many decimal amounts. Real prices are often stored as integers in the smallest currency unit or with a clearly defined rounding rule BigDecimal.

2. "Default Constructor" Appears Only When No Constructor Is Declared ​

java
class Draft {
    String title;
}

The compiler provides a default no-argument constructor for Draft. If a class declares any constructor, the compiler no longer adds this one:

java
class Book {
    Book(String title) { /* ... */ }
}

// new Book(); // Compilation error: no applicable no-argument constructor

A "default constructor" specifically refers to the one implicitly provided by the compiler. The Book() {} you write yourself is an explicit no-argument constructor and should not be confused with a default constructor.

3. Centralize Initialization with Overloads and this(...) ​

java
class Book {
    private final String title;
    private final String author;
    private final double price;

    Book(String title, String author) {
        this(title, author, 0.0);
    }

    Book(String title, String author, double price) {
        if (title == null || title.isBlank()) {
            throw new IllegalArgumentException("blank title");
        }
        if (author == null || author.isBlank()) {
            throw new IllegalArgumentException("blank author");
        }
        if (!Double.isFinite(price) || price < 0.0) {
            throw new IllegalArgumentException("invalid price");
        }
        this.title = title;
        this.author = author;
        this.price = price;
    }
}

Multiple constructors are overloaded based on their parameter lists. this(...) calls another constructor of the same class, and in Java 17, this must be the first statement in the constructor. Additionally, a constructor call chain cannot form a cycle.

If a constructor does not explicitly write this(...) or super(...), the compiler will attempt to insert an implicit super(). Therefore, if the parent class does not have an accessible no-argument constructor, the child class must explicitly select another parent constructor. This topic will be further expanded in the inheritance chapter.

4. Initialization Order ​

When creating instances of a regular subclass, you can troubleshoot issues by following this conceptual order:

  1. Assign default values to the instance fields of the new object.
  2. Follow any this(...) delegation to the constructor that calls super(...), then execute the superclass constructor chain.
  3. Follow the source code order to execute the instance field initializers and instance initialization blocks of the current class.
  4. Execute the remaining statements in the constructor that called the superclass; then return through any this(...) delegation chain, executing each caller’s remaining statements.

Details of the constructor chain are influenced by the class hierarchy and explicit constructor calls. For beginners, the safest approach is to consolidate shared validation into a single main constructor to avoid creating implicit order dependencies through multiple initialization blocks.

5. Failed Construction Does Not Return a Usable Object ​

If validation throws an exception, the new Book(...) expression does not normally return a reference:

java
try {
    Book invalid = new Book(" ", "A certain author", 10.0);
} catch (IllegalArgumentException error) {
    System.out.println(error.getMessage());
}

Do not publish the this to global collections, callbacks, or other threads before the constructor is complete, otherwise external code may observe an object that has not yet been initialized.

Also, do not call instance methods that can be overridden by subclasses from within a constructor. Dynamic dispatch may invoke the subclass implementation, while the subclass fields have not yet been initialized. The lab will demonstrate this failure in practice.

6. Constructors and Factory Methods Serve Different Needs ​

Constructors are direct and can be invoked directly via new. Static factory methods can have meaningful names, return cached instances, or return subtypes of the declared type:

java
static Book freeSample(String title, String author) {
    return new Book(title, author, 0.0);
}

Do not wrap every simple constructor in a factory just for the sake of "design patterns." Only use a factory when the name, caching, return type, or creation process truly requires it.

Professional Quick Reference ​

RequirementApproach
Mandatory fieldsProvide parameterized constructor only
Preserve no-arg creationExplicitly declare no-arg constructor
Reuse initializationthis(...) Link to main constructor
Reject illegal statesValidate and assign final fields after check
Multiple named creation methodsStatic factory methods
Exact amountlong minimum unit or BigDecimal

Before Moving On ​

Try empty book names, null author, negative prices, NaN, and infinite prices to ensure that failed constructions do not return references normally. Then explain the difference between the default constructor and the explicit no-arg constructor, this(...) why it must be executed first, and what state is exposed if this is published or if an override method is called during construction.

→ Next: Encapsulation and Access Boundaries

Specification: JLS §12.5: Creation and Initialization of New Objects.

Built with VitePress | Software Systems Atlas