7.3 Constructors and Object Invariants
Prerequisites: Classes, Fields, Methods, and
thisEstimated 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
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
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:
class Book {
Book(String title) { /* ... */ }
}
// new Book(); // Compilation error: no applicable no-argument constructorA "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(...)
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:
- Assign default values to the instance fields of the new object.
- Follow any
this(...)delegation to the constructor that callssuper(...), then execute the superclass constructor chain. - Follow the source code order to execute the instance field initializers and instance initialization blocks of the current class.
- 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:
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:
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
| Requirement | Approach |
|---|---|
| Mandatory fields | Provide parameterized constructor only |
| Preserve no-arg creation | Explicitly declare no-arg constructor |
| Reuse initialization | this(...) Link to main constructor |
| Reject illegal states | Validate and assign final fields after check |
| Multiple named creation methods | Static factory methods |
| Exact amount | long 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.