The previous lesson ended with an uncomfortable loose end: new Loan() produces a loan with no book, no employee and zero elapsed days, and to make it usable you have to fill in its fields one by one from outside, trusting that you forget none of them. That half-built object is a time bomb: between the new and the last assignment there is an interval in which the object is invalid, and if anyone uses it there, they get a NullPointerException or, worse, a silently incorrect result. Constructors solve exactly that problem. A constructor is the block of code that runs when an object is created, and its mission is to guarantee that no object is born in an invalid state. In this lesson you will learn to write them, overload them, chain them, control the exact order in which everything is initialised and validate the input data. By the end, Book, Employee and Loan will be objects that are born complete and consistent.
Contents
- What a constructor is
- Constructor versus method: seven differences
- The default constructor and when it disappears
- Constructor overloading
- Chaining with
this(...)and its rules - Order of initialisation, step by step
finalfields and their mandatory initialisation- The telescoping constructor and the Builder pattern
- Validating in the constructor
- The copy constructor
staticinitialisation blocks- BiblioTech:
Book,EmployeeandLoanwith complete constructors - Common Mistakes and Tips
- Exercises
- What a constructor is
A constructor is a block with the same name as the class and no return type, which runs automatically when an object is created with new:
public class Book {
public String title;
public String author;
public String isbn;
/** Constructor: runs when you do new Book(...). */
public Book(String title, String author, String isbn) {
this.title = title;
this.author = author;
this.isbn = isbn;
}
}And from that moment on, creating a book is a single line that cannot be left half-done:
Compare it with what you were doing until now:
// BEFORE: four lines, and nothing forces you to write all four
Book effectiveJava = new Book();
effectiveJava.title = "Effective Java";
effectiveJava.author = "Joshua Bloch";
effectiveJava.isbn = "978-0000000001";The difference is not brevity, it is guarantees. With the constructor, the compiler demands all three pieces of data: it is literally impossible to create a Book without a title. That principle has a name and it will stay with you your whole career: make invalid states unrepresentable.
Notice too the use of this (lesson 03-03): the parameters have the same names as the fields —a universal convention in Java— and this.title distinguishes the field from the parameter shadowing it.
- Constructor versus method: seven differences
| Aspect | Constructor | Method |
|---|---|---|
| Name | Identical to the class, initial capital | Anything, in lowerCamelCase |
| Return type | None (not even void) |
Mandatory (void or a type) |
| When it runs | Only when creating the object with new |
When it is explicitly called |
| Is it inherited | No (03-05) | Yes |
Can it be static |
No | Yes |
return |
Only return; to exit early |
return value; according to the type |
| If you do not write it | Java gives you a default one | Nothing appears |
The most common mistake of all is this one:
Adding void turns the constructor into an ordinary method that happens to be called Book. It compiles without protest, new Book("...") stops compiling and the programmer goes mad looking for the reason. Rule: a constructor never has a return type, ever.
- The default constructor and when it disappears
If a class declares no constructor, Java adds one automatically: the default constructor, with no parameters and an empty body.
But the moment you declare any constructor, the gift disappears:
public class Book {
public Book(String title) { this.title = title; }
}
Book b = new Book(); // compile ERRORerror: constructor Book in class Book cannot be applied to given types; required: String found: no arguments
It is deliberate behaviour: if you have decided that a book needs a title, Java is not going to let anyone skip it. If you also want to allow creation without arguments, you must declare it explicitly:
- Constructor overloading
Constructors are overloaded just like methods: same name, different parameter list. It serves to offer several ways of creating the same object.
public class Book {
public String title;
public String author;
public String isbn;
public int publicationYear;
public boolean available;
/** Full constructor. */
public Book(String title, String author, String isbn,
int publicationYear, boolean available) {
this.title = title;
this.author = author;
this.isbn = isbn;
this.publicationYear = publicationYear;
this.available = available;
}
/** Usual constructor: a new book comes in available. */
public Book(String title, String author, String isbn, int publicationYear) {
this(title, author, isbn, publicationYear, true);
}
/** Minimal constructor: for quick catalog entries. */
public Book(String title, String isbn) {
this(title, "Unknown", isbn, 0, true);
}
}Three ways of creating a book, all of them consistent:
Book a = new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018, true);
Book b = new Book("Design Patterns", "Erich Gamma", "978-0000000002", 1994);
Book c = new Book("Refactoring", "978-0000000003");
- Chaining with
this(...) and its rules
this(...) and its rulesIn the previous example this(...) appears: one constructor calling another constructor of the same class. It is the mechanism that avoids duplicating initialisation code.
flowchart LR
A["new Book(title, isbn)"] --> B["Book(String, String)"]
B -->|"this(...)"| C["Book(String, String, String, int, boolean)
full constructor:
assigns every field"]
The professional pattern is to have a single constructor that really assigns (sometimes called the canonical constructor) and to have all the others funnel into it via this(...). That way the validation is written only once.
Strict rules imposed by the compiler:
this(...)must be the first statement of the constructor. Not even aSystem.out.printlnbefore it.- You cannot have both
this(...)andsuper(...)(lesson 03-05): only one of the two, and always first. - Cycles are not allowed: if
A()callsB()andB()callsA(), the compiler detects it and fails with "recursive constructor invocation".
public Book(String title, String isbn) {
System.out.println("Creating book..."); // ERROR if it goes first
this(title, "Unknown", isbn, 0, true);
}
- Order of initialisation, step by step
When you run new, several things happen in a fixed order that is worth knowing, because it explains apparently magical behaviour. The stages are:
- Memory is reserved and all the fields take their default value (
0,false,null). - The field initialisers and the instance initialisation blocks run, in the textual order in which they appear in the file.
- The body of the constructor runs.
A traced example that demonstrates it:
public class OrderDemo {
public int a = show("1. initialiser of field a");
{
show("2. instance initialisation block");
}
public int b = show("3. initialiser of field b");
public OrderDemo() {
show("4. body of the constructor");
}
private static int show(String message) {
System.out.println(message);
return 0;
}
public static void main(String[] args) {
new OrderDemo();
}
}Output:
1. initialiser of field a 2. instance initialisation block 3. initialiser of field b 4. body of the constructor
Two important conclusions:
- The initialisers and blocks run before the body of the constructor, always, and top to bottom. If you have three overloaded constructors, that common code runs in all three.
- The instance initialisation blocks (the loose braces) exist for code common to every constructor, but in practice they are little used: a canonical constructor that the others chain to with
this(...)is clearer.
final fields and their mandatory initialisation
final fields and their mandatory initialisationYou already know that final prevents a variable from being reassigned (module 1). Applied to a field, it means that its value is set once and does not change during the object's life:
public class Book {
public final String isbn; // has no initialiser
public Book(String isbn) {
this.isbn = isbn; // must be assigned here
}
}A final field must be assigned exactly once by the time every constructor finishes, whether in its declaration, in an initialisation block or in the constructor body. The compiler checks all three routes:
public class Book {
public final String isbn;
public Book() { } // ERROR: variable isbn might not have been initialized
}public final String isbn = "000";
public Book(String isbn) {
this.isbn = isbn; // ERROR: cannot assign a value to final variable isbn
}final fields are a first-class design tool: they express in the code itself that a piece of data is the object's identity and not something that can be changed. A book's ISBN never changes; neither does its title. What does change is available. That distinction, marked with final, documents the model better than any comment, and it lays the ground for the immutability you will study in 03-07.
- The telescoping constructor and the Builder pattern
When a class has many optional fields, overloading degenerates into what is known as the telescoping constructor:
public Book(String title) { ... }
public Book(String title, String author) { ... }
public Book(String title, String author, String isbn) { ... }
public Book(String title, String author, String isbn, int year) { ... }
public Book(String title, String author, String isbn, int year, boolean avail) { ... }
public Book(String title, String author, String isbn, int year, boolean avail, String publisher) { ... }The problems are real and serious:
- Unreadable calls. What does
new Book("Effective Java", "Bloch", "978-...", 2018, true, "Addison")mean? You have to open the class to find out. - Silent errors from ordering. If you swap two parameters of the same type —
titleandisbn, bothString—, it compiles perfectly and the bug shows up in production. - Combinatorial explosion. With six optional fields you would need dozens of constructors.
A partial and cheap improvement: name the variables before calling.
String title = "Effective Java";
String isbn = "978-0000000001";
Book b = new Book(title, "Joshua Bloch", isbn, 2018, true);The complete solution is the Builder pattern, which lets you write something like:
Book book = new Book.Builder("Effective Java", "978-0000000001")
.author("Joshua Bloch")
.publicationYear(2018)
.available(true)
.build();Every piece of data is named at the call site, the order stops mattering and the optional ones are omitted. It requires static nested classes, studied in 04-03, so the pattern is developed in lesson 12-02 (Design Patterns). For now, take away the practical rule: if you need more than four or five parameters in a constructor, it is a sign that data needs grouping into objects or that you need a Builder.
- Validating in the constructor
If the constructor is the gatekeeper at the object's entrance, it must reject absurd data. A publication year of -500, a negative rate or an empty ISBN must not be allowed in.
Important note on the mechanism. The professional way to reject an invalid argument is to throw an exception (
throw new IllegalArgumentException(...)), which prevents the object from ever existing. But exceptions are module 6 and we have not studied them yet. That is why, during this module, validation will consist of warning on the console and applying a safe default value. It is a provisional and deliberately imperfect solution: remember it, because in module 6 we will come back to these very classes to do it properly.
public Book(String title, String author, String isbn,
int publicationYear, boolean available) {
// --- Title validation ---
if (title == null || title.isBlank()) {
System.out.println("WARNING: empty title. 'Untitled' will be used.");
this.title = "Untitled";
} else {
this.title = title.trim();
}
// --- Author validation ---
this.author = (author == null || author.isBlank()) ? "Unknown" : author.trim();
// --- ISBN validation ---
if (isbn == null || isbn.isBlank()) {
System.out.println("WARNING: empty ISBN. '000-0000000000' will be used.");
this.isbn = "000-0000000000";
} else {
this.isbn = isbn.trim();
}
// --- Year validation ---
if (publicationYear < 1450 || publicationYear > 2100) {
System.out.println("WARNING: year " + publicationYear + " out of range. 0 will be used.");
this.publicationYear = 0;
} else {
this.publicationYear = publicationYear;
}
this.available = available;
booksCreated++;
}Notice three details: isBlank() (Java 11+) is better than isEmpty() because it also detects strings made only of spaces; the null check goes before the content check, taking advantage of the short-circuiting of || you studied in module 1; and trim() normalises the input, preventing "Effective Java " and "Effective Java" from being treated as different titles.
And here is the other big benefit: the counter booksCreated++ now lives inside the constructor, not scattered around main. It is impossible to create a book without counting it.
- The copy constructor
A copy constructor receives an object of the same class and creates a new one with the same values. It is the solution to the aliasing problem from lesson 03-02: when you really want an independent object.
/**
* Copy constructor: creates a new copy with the same bibliographic data
* as another one. Useful for registering duplicates in the catalog.
*/
public Book(Book other) {
this(other.title, other.author, other.isbn, other.publicationYear, other.available);
}Use:
Book original = new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018);
Book secondCopy = new Book(original);
secondCopy.available = false;
System.out.println(original.available); // true <-- independent
System.out.println(original == secondCopy); // false <-- different objectsCompare it with Book alias = original;, which copied nothing. Here there really are two objects on the heap.
A nuance that will come back in 03-07: this copy is shallow. It copies the values of the fields; if any field were a reference to a mutable object, both copies would share that inner object. With String and int there is no problem, because they are immutable or primitive.
static initialisation blocks
static initialisation blocksJust as there are instance initialisation blocks, there are static blocks, which run only once, when the JVM loads the class, before any object is created:
public class Loan {
public static final int LOAN_DAYS = 15;
public static final double DAILY_RATE = 0.25;
public static final double MAX_FINE = 20.0;
public static final int MINOR_THRESHOLD = 7;
/** Days late from which the fine cap is reached. */
public static final int DAYS_TO_CAP;
static {
// Calculated only once, when the class is loaded.
DAYS_TO_CAP = (int) Math.ceil(MAX_FINE / DAILY_RATE);
System.out.println("[Loan] Rules loaded. Cap at "
+ DAYS_TO_CAP + " days late.");
}
}When you run anything that touches Loan, you will see once:
The global order, summarised in a table:
| Moment | What runs | How many times |
|---|---|---|
| Class loading | static initialisers and static blocks, in textual order |
Once per run |
new |
Default field values → instance initialisers and { } blocks → constructor body |
Once per object |
static blocks are mainly used to initialise computed constants or load configuration. Use them sparingly: code that runs "by itself" is hard to debug.
- BiblioTech:
Book, Employee and Loan with complete constructors
Book, Employee and Loan with complete constructorsLet us apply all of the above to the three domain classes.
Book
package com.nexussoftware.bibliotech.domain;
/**
* Book in Nexus Software's technical catalog.
* The bibliographic data is immutable (final); only availability changes.
*/
public class Book {
public static int booksCreated = 0;
public final String title;
public final String author;
public final String isbn;
public final int publicationYear;
public boolean available; // the only mutable field
/** Canonical constructor: the only one that assigns and validates. */
public Book(String title, String author, String isbn,
int publicationYear, boolean available) {
this.title = (title == null || title.isBlank()) ? "Untitled" : title.trim();
this.author = (author == null || author.isBlank()) ? "Unknown" : author.trim();
if (isbn == null || isbn.isBlank()) {
System.out.println("WARNING: empty ISBN in '" + this.title + "'.");
this.isbn = "000-0000000000";
} else {
this.isbn = isbn.trim();
}
if (publicationYear < 1450 || publicationYear > 2100) {
System.out.println("WARNING: invalid year (" + publicationYear + ") in '"
+ this.title + "'. Recorded as 0.");
this.publicationYear = 0;
} else {
this.publicationYear = publicationYear;
}
this.available = available;
booksCreated++;
}
/** Usual entry: a new book comes in available. */
public Book(String title, String author, String isbn, int publicationYear) {
this(title, author, isbn, publicationYear, true);
}
/** Copy constructor: a new copy with the same data. */
public Book(Book other) {
this(other.title, other.author, other.isbn, other.publicationYear, other.available);
}
public boolean isAvailable() { return available; }
public void lend() {
if (!available) {
System.out.println("WARNING: '" + title + "' was already on loan.");
return;
}
available = false;
}
public void returnItem() {
if (available) {
System.out.println("WARNING: '" + title + "' was already available.");
return;
}
available = true;
}
public String describe() {
return title + " - " + author + " (" + publicationYear + "), ISBN " + isbn;
}
}Employee
package com.nexussoftware.bibliotech.domain;
/** Nexus Software employee authorised to take books on loan. */
public class Employee {
public static final int MAX_CONCURRENT_LOANS = 3;
public static int employeesRegistered = 0;
public final String name;
public final String identifier;
public int totalLoans;
public Employee(String name, String identifier, int totalLoans) {
this.name = (name == null || name.isBlank()) ? "Unnamed employee"
: name.trim();
if (identifier == null || !identifier.startsWith("EMP-")) {
System.out.println("WARNING: invalid identifier for " + this.name
+ ". EMP-000 is assigned.");
this.identifier = "EMP-000";
} else {
this.identifier = identifier.trim();
}
if (totalLoans < 0) {
System.out.println("WARNING: negative loans for " + this.name + ". 0 is used.");
this.totalLoans = 0;
} else {
this.totalLoans = totalLoans;
}
employeesRegistered++;
}
/** Usual entry: new employee, no loans. */
public Employee(String name, String identifier) {
this(name, identifier, 0);
}
public boolean canBorrow() {
return totalLoans < MAX_CONCURRENT_LOANS;
}
public void registerLoan() {
if (!canBorrow()) {
System.out.printf("WARNING: %s already has %d loans (maximum %d).%n",
name, totalLoans, MAX_CONCURRENT_LOANS);
return;
}
totalLoans++;
}
public void registerReturn() {
totalLoans = Math.max(0, totalLoans - 1);
}
}Loan
Here comes the promised novelty: the loan calculates its own due day when it is born, from LOAN_DAYS.
We work with whole days, as in the whole course so far:
loanDayis the day in BiblioTech's internal calendar on which the book was handed over (for example, operating day 100). Real dates withLocalDatearrive in lesson 10-05.
package com.nexussoftware.bibliotech.domain;
/**
* Record of a loan of a book to an employee.
* It is born with a book, an employee and a loan day, and computes its own due day.
*/
public class Loan {
public static final int LOAN_DAYS = 15;
public static final double DAILY_RATE = 0.25;
public static final double MAX_FINE = 20.0;
public static final int MINOR_THRESHOLD = 7;
/** Days late from which the fine reaches the cap. */
public static final int DAYS_TO_CAP;
static {
DAYS_TO_CAP = (int) Math.ceil(MAX_FINE / DAILY_RATE);
}
public static int loansCreated = 0;
public final Book book;
public final Employee employee;
public final int loanDay;
public final int dueDay; // derived: not asked for, computed
public final String reference; // readable identifier of the loan
public int elapsedDays;
public boolean returned;
/** Canonical constructor. */
public Loan(Book book, Employee employee, int loanDay, int elapsedDays) {
if (book == null) {
System.out.println("WARNING: loan with no book. A placeholder is recorded.");
this.book = new Book("Unknown book", "Unknown", "000-0000000000", 0, false);
} else {
this.book = book;
}
if (employee == null) {
System.out.println("WARNING: loan with no employee. A placeholder is recorded.");
this.employee = new Employee("Unknown employee", "EMP-000");
} else {
this.employee = employee;
}
this.loanDay = Math.max(0, loanDay);
this.dueDay = this.loanDay + LOAN_DAYS; // business rule
if (elapsedDays < 0) {
System.out.println("WARNING: negative elapsed days. 0 is used.");
this.elapsedDays = 0;
} else {
this.elapsedDays = elapsedDays;
}
this.returned = false;
loansCreated++;
this.reference = String.format("LN-%04d", loansCreated);
// Effects on the related objects
this.book.lend();
this.employee.registerLoan();
}
/** Loan just made: zero elapsed days. */
public Loan(Book book, Employee employee, int loanDay) {
this(book, employee, loanDay, 0);
}
// ---------- Behaviour (lesson 03-03) ----------
public int calculateDaysLate() {
return Math.max(0, elapsedDays - LOAN_DAYS);
}
public double calculateFine() {
return Math.min(calculateDaysLate() * DAILY_RATE, MAX_FINE);
}
public String classifySeverity() {
int late = calculateDaysLate();
if (late == 0) return "ON TIME";
if (late <= MINOR_THRESHOLD) return "MINOR";
return "SEVERE";
}
public boolean isOverdue() { return calculateDaysLate() > 0; }
public int daysRemaining() { return Math.max(0, LOAN_DAYS - elapsedDays); }
public int expectedReturnDay() { return dueDay; }
public static boolean isMinorDelay(int daysLate) {
return daysLate > 0 && daysLate <= MINOR_THRESHOLD;
}
}Points that deserve attention:
dueDayis a derived field: it is not asked of the caller, the constructor computes it. It is impossible to create a loan with a due day inconsistent with its start date.referencegenerates itself from the static counter, withString.format(module 1):LN-0001,LN-0002... Nobody can invent a reference.- The constructor has effects on other objects: it marks the book as on loan and adds a loan to the employee. This is debatable in advanced design (a constructor "that does things" is surprising), but here it guarantees the system's consistency. Remember it when the separation between constructing and executing appears in module 12.
Use in BiblioTechApp
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.domain.Employee;
import com.nexussoftware.bibliotech.domain.Book;
import com.nexussoftware.bibliotech.domain.Loan;
public class BiblioTechApp {
public static void main(String[] args) {
System.out.println("=== BiblioTech 3.1 - Objects born complete ===\n");
// Catalog
Book effectiveJava = new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018);
Book designPatterns = new Book("Design Patterns", "Erich Gamma", "978-0000000002", 1994);
Book refactoring = new Book("Refactoring", "Martin Fowler", "978-0000000003", 2018);
// Staff
Employee marta = new Employee("Marta Ruiz", "EMP-001");
Employee diego = new Employee("Diego Alonso", "EMP-002");
Employee nuria = new Employee("Nuria Vidal", "EMP-003");
// Loans: day 100 of the internal calendar
Loan l1 = new Loan(effectiveJava, marta, 100, 20);
Loan l2 = new Loan(designPatterns, diego, 100, 10);
Loan l3 = new Loan(refactoring, nuria, 100, 95);
System.out.printf("%-8s %-22s %-14s %5s %5s %8s %9s %s%n",
"REF", "BOOK", "EMPLOYEE", "START", "DUE", "ELAPSED", "FINE", "SEVERITY");
print(l1);
print(l2);
print(l3);
System.out.printf("%nBooks created: %d | Employees: %d | Loans: %d%n",
Book.booksCreated, Employee.employeesRegistered,
Loan.loansCreated);
System.out.printf("Fine cap reached at %d days late.%n",
Loan.DAYS_TO_CAP);
}
private static void print(Loan l) {
System.out.printf("%-8s %-22s %-14s %5d %5d %8d %9.2f %s%n",
l.reference, l.book.title, l.employee.name,
l.loanDay, l.dueDay, l.elapsedDays,
l.calculateFine(), l.classifySeverity());
}
}Output:
=== BiblioTech 3.1 - Objects born complete === REF BOOK EMPLOYEE START DUE ELAPSED FINE SEVERITY LN-0001 Effective Java Marta Ruiz 100 115 20 1.25 MINOR LN-0002 Design Patterns Diego Alonso 100 115 10 0.00 ON TIME LN-0003 Refactoring Nuria Vidal 100 115 95 20.00 SEVERE Books created: 3 | Employees: 3 | Loans: 3 Fine cap reached at 80 days late.
Compare the start of main with the one from lesson 03-02: six lines of creation instead of thirty, and not a single chance of forgetting a field.
Common Mistakes and Tips
- Giving the constructor a return type.
public void Book(...)is a method, not a constructor. It is mistake number one, and the compiler does not help you: the failure shows up later, when you usenew. - Losing the default constructor without noticing. As soon as you declare one with parameters,
new Book()stops compiling. If you need it, declare it yourself. - Putting something before
this(...). It does not compile. If you need to run common code first, it goes inside the canonical constructor. - Calling overridable methods from the constructor. It is a subtle problem that is fully understood in 03-05: if a subclass overrides that method, it will run before the subclass's fields are initialised, seeing
nulls and zeros. Rule: from a constructor, call onlyprivateorfinalmethods. - Assigning without
thiswhen there is shadowing.title = title;does nothing. The IDE flags it; do not ignore the warning. - Duplicating the validation in every overloaded constructor. Chain with
this(...)up to the canonical constructor, and validate there only once. - Constructors with heavy side effects. Reading a file or opening a connection in a constructor makes the class impossible to test. A constructor must initialise, not work.
- Tip: make
finalevery field that must not change. It is free, the compiler checks it and it documents the design better than a comment. - Tip: let the IDE generate the constructor (
Alt+Insert→ Constructor in IntelliJ,Alt+Shift+S→ Generate Constructor using Fields in Eclipse) and then add the validation by hand.
Exercises
Exercise 1: constructors for a Reservation class
Picking up the meeting rooms model from lesson 03-01, create a Reservation class with final fields for room (String), requester (Employee), startHour (int, 0-23), endHour (int) and attendees (int), plus a mutable field active (boolean) and a code generated automatically with the format RES-0001.
Requirements:
- A canonical constructor that validates: the hours within 0-23,
endHourlater thanstartHourand attendees greater than zero. Apply safe default values and warn on the console. - An overloaded constructor that assumes a duration of one hour (it receives only the start hour).
- A
staticcounter of created reservations that feeds the code. - A
durationHours()method.
Create three reservations, one of them with invalid data, and show the result.
Exercise 2: trace the order of initialisation
Write a class ConstructionTrace that prints a message at each of these points: a static block, a static field initialiser, an instance field initialiser, an instance initialisation block and two constructors chained with this(...). Create two objects from main and predict the output before running it. Then explain: which messages appear only once and which twice? Why?
Exercise 3: copy constructor and aliasing
Write a static method in BiblioTechApp called duplicateCopy(Book original) that returns a new Book with the same data but marked as available. Demonstrate with console output that:
- The duplicate and the original are different objects (
==givesfalse). - Changing the availability of one does not affect the other.
- By contrast,
Book alias = original;does produce interference.
Then answer: if Book had a field Employee lastReader (a mutable reference), would the copy constructor as written be enough? What problem would appear?
Solutions
Solution 1
package com.nexussoftware.bibliotech.domain;
/** Reservation of a Nexus Software meeting room. */
public class Reservation {
public static int reservationsCreated = 0;
public final String room;
public final Employee requester;
public final int startHour;
public final int endHour;
public final int attendees;
public final String code;
public boolean active;
/** Canonical constructor: the only one that assigns and validates. */
public Reservation(String room, Employee requester,
int startHour, int endHour, int attendees) {
this.room = (room == null || room.isBlank()) ? "Unnamed room" : room.trim();
if (requester == null) {
System.out.println("WARNING: reservation with no requester. A placeholder is recorded.");
this.requester = new Employee("Unknown employee", "EMP-000");
} else {
this.requester = requester;
}
// Start hour within the range of the day
if (startHour < 0 || startHour > 23) {
System.out.println("WARNING: invalid start hour (" + startHour + "). 9 is used.");
this.startHour = 9;
} else {
this.startHour = startHour;
}
// End hour: valid and later than the start
if (endHour < 0 || endHour > 23 || endHour <= this.startHour) {
int proposed = Math.min(23, this.startHour + 1);
System.out.println("WARNING: invalid end hour (" + endHour
+ "). " + proposed + " is used.");
this.endHour = proposed;
} else {
this.endHour = endHour;
}
if (attendees <= 0) {
System.out.println("WARNING: invalid number of attendees. 1 is used.");
this.attendees = 1;
} else {
this.attendees = attendees;
}
this.active = true;
reservationsCreated++;
this.code = String.format("RES-%04d", reservationsCreated);
}
/** One-hour reservation: only the start is given. */
public Reservation(String room, Employee requester, int startHour, int attendees) {
this(room, requester, startHour, startHour + 1, attendees);
}
public int durationHours() {
return endHour - startHour;
}
public void cancel() {
if (!active) {
System.out.println("WARNING: reservation " + code + " was already cancelled.");
return;
}
active = false;
}
}Test:
Employee marta = new Employee("Marta Ruiz", "EMP-001");
Reservation r1 = new Reservation("North Room", marta, 9, 11, 6);
Reservation r2 = new Reservation("South Room", marta, 16, 4); // one hour
Reservation r3 = new Reservation("East Room", marta, 25, 3, -2); // invalid data
System.out.printf("%-9s %-12s %5s %5s %6s %11s%n",
"CODE", "ROOM", "START", "END", "PEOPLE", "DURATION");
System.out.printf("%-9s %-12s %5d %5d %6d %8d h%n",
r1.code, r1.room, r1.startHour, r1.endHour, r1.attendees, r1.durationHours());
System.out.printf("%-9s %-12s %5d %5d %6d %8d h%n",
r2.code, r2.room, r2.startHour, r2.endHour, r2.attendees, r2.durationHours());
System.out.printf("%-9s %-12s %5d %5d %6d %8d h%n",
r3.code, r3.room, r3.startHour, r3.endHour, r3.attendees, r3.durationHours());Output:
WARNING: invalid start hour (25). 9 is used. WARNING: invalid end hour (3). 10 is used. WARNING: invalid number of attendees. 1 is used. CODE ROOM START END PEOPLE DURATION RES-0001 North Room 9 11 6 2 h RES-0002 South Room 16 17 4 1 h RES-0003 East Room 9 10 1 1 h
A subtle detail: the validation of endHour compares against this.startHour (the already validated one), not against the parameter startHour. If it compared against the parameter, a corrected start hour could leave an inconsistent interval. The order of the validations matters.
Another detail: the one-hour constructor chains with this(...) passing startHour + 1, so if the start is invalid, the correction happens only once, in the canonical one.
Solution 2
package com.nexussoftware.bibliotech;
public class ConstructionTrace {
static int staticCounter = show("A. static field initialiser");
static {
show("B. static block");
}
int instanceField = show("C. instance field initialiser");
{
show("D. instance initialisation block");
}
public ConstructionTrace() {
this(0); // must be the FIRST statement
show("F. body of the no-argument constructor");
}
public ConstructionTrace(int value) {
show("E. body of the canonical constructor");
}
private static int show(String message) {
System.out.println(message);
return 0;
}
public static void main(String[] args) {
System.out.println("--- first object ---");
new ConstructionTrace();
System.out.println("--- second object ---");
new ConstructionTrace();
}
}Output:
A. static field initialiser B. static block --- first object --- C. instance field initialiser D. instance initialisation block E. body of the canonical constructor F. body of the no-argument constructor --- second object --- C. instance field initialiser D. instance initialisation block E. body of the canonical constructor F. body of the no-argument constructor
Explanation. A and B appear only once and before everything else: they are class code, and the JVM runs it when loading the class, which happens at the first reference to ConstructionTrace (here, running main itself). C, D, E and F appear twice, once per object: they are instance code.
Two nuances that are surprising and worth internalising. First, C and D run before any constructor body, in textual order. Second, although the argument-less new calls the no-parameter constructor, message E appears before F: this(0) transfers control to the canonical constructor, which runs in full, and only afterwards does the original constructor's body continue.
Solution 3
/**
* Creates a new copy with the same bibliographic data as the original,
* always available for loan.
*/
private static Book duplicateCopy(Book original) {
Book copy = new Book(original); // copy constructor
copy.available = true; // the new copy goes on the shelf
return copy;
}Demonstration:
Book original = new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018);
original.lend(); // original becomes NOT available
Book duplicate = duplicateCopy(original);
Book alias = original;
System.out.println("1) original == duplicate : " + (original == duplicate)); // false
System.out.println(" original == alias : " + (original == alias)); // true
System.out.println("2) before -> original: " + original.isAvailable()
+ " | duplicate: " + duplicate.isAvailable());
duplicate.lend();
System.out.println(" after -> original: " + original.isAvailable()
+ " | duplicate: " + duplicate.isAvailable());
System.out.println("3) alias.returnItem() ...");
alias.returnItem();
System.out.println(" original.isAvailable(): " + original.isAvailable());Output:
1) original == duplicate : false original == alias : true 2) before -> original: false | duplicate: true after -> original: false | duplicate: false 3) alias.returnItem() ... original.isAvailable(): true
Reading: the duplicate is an independent object (steps 1 and 2), whereas the alias is the same memory box under another name, so alias.returnItem() changes the state that original sees (step 3). It is exactly the distinction from lesson 03-02.
Answer to the final question. No, it would not be enough. The copy constructor as written makes a shallow copy: it copies the value of each field, and the value of a reference-type field is the address. If Book had Employee lastReader, the original and the duplicate would point to the same Employee object, so duplicate.lastReader.registerLoan() would also alter the employee the original sees. It is aliasing sneaking in through the back door.
There are three possible ways out, and you will see them developed in 03-07:
- Deep copy: also create a new
Employeein the copy constructor (this.lastReader = new Employee(other.lastReader);). Expensive, and it duplicates entities that perhaps ought to be unique. - Share on purpose: if the
Employeeis an entity shared by the whole system, sharing the reference is the right thing; what must be avoided is somebody mutating it through the book. - Make the referenced class immutable, after which sharing stops being dangerous because nobody can change it.
That is the deep reason why immutability simplifies design so much: when nothing can mutate, the difference between copying and sharing disappears.
Conclusion
You have shut the door on incomplete objects. You know what a constructor is and how it differs from a method —starting with the detail that causes the most headaches, that it carries no return type—; you know the default constructor and the exact moment Java stops giving it to you. You know how to overload constructors to offer several ways of creating the same object and how to chain them with this(...) towards a canonical constructor that validates only once, with its strict rules. You have traced the complete order of initialisation —static blocks when the class is loaded; then, for each new, default values, instance initialisers and finally the constructor body— and you understand why a final field forces the compiler to check every route. You know the telescoping constructor problem and you know that the Builder pattern solves it in lesson 12-02. You validate in the constructor, provisionally with warnings and default values, knowing that module 6 will bring the correct solution with exceptions. And you master the copy constructor, the only real way to duplicate an object, with the warning that it is shallow.
BiblioTech has taken a leap: Book, Employee and Loan are born complete, with their identity data marked final, with input validation, with counters that cannot be forgotten and with a Loan that computes its own due day and its reference LN-0001 without anyone dictating them.
In the next lesson, Inheritance, the model will grow upwards. Nexus Software does not only lend books: it also lends technical magazines and training DVDs, each with its own term and rate. Instead of duplicating Book three times, you will create a base class Material with what is common and three specialisations that only declare their differences. You will learn extends, super, the order of construction in a hierarchy and the difference —an endless source of confusion— between overriding and overloading.
Java Programming Course
Module 1: Introduction to Java
- Introduction to Java
- Setting Up the Development Environment
- Basic Syntax and Structure
- Variables and Data Types
- Operators
- Console Input and Output
- Your First Complete Program: BiblioTech
Module 2: Control Flow
- Conditional Statements
- Loops
- Switch Statements
- Break and Continue
- Debugging and Execution Traces
- Project: The BiblioTech Interactive Menu
Module 3: Object-Oriented Programming
- Introduction to OOP
- Classes and Objects
- Methods
- Constructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- The Object Class: equals, hashCode and toString
Module 4: Advanced Object-Oriented Programming
- Interfaces
- Abstract Classes
- Inner Classes
- Anonymous Classes
- Lambda Expressions
- Functional Interfaces and Method References
- Enums and Records
Module 5: Data Structures and Collections
- Arrays
- The Collections Framework
- ArrayList
- LinkedList
- HashMap
- HashSet
- Queue and Deque
- Stack
- Sorting and Searching Collections
Module 6: Exception Handling
- Introduction to Exceptions
- The Try-Catch Block
- Throw and Throws
- Custom Exceptions
- The Finally Block
- Try-with-resources and AutoCloseable
- Error Handling Strategies and Logging
Module 7: File Input/Output
- Reading Files
- Writing Files
- File Streams
- BufferedReader and BufferedWriter
- Serialization
- The NIO.2 API: Path and Files
- Interchange Formats: CSV and Properties
Module 8: Multithreading and Concurrency
- Introduction to Multithreading
- Creating Threads
- Thread Lifecycle
- Synchronization
- Concurrency Utilities
- Concurrent Collections and Atomic Variables
- Asynchronous Tasks with CompletableFuture
Module 9: Networking
- Introduction to Networking
- Sockets
- ServerSocket
- DatagramSocket and DatagramPacket
- URL and HttpURLConnection
- The Modern HTTP Client
Module 10: Advanced Topics
- Generics
- Annotations
- Reflection
- Java 8 Features: Streams and Optional
- Dates and Times with java.time
- Java 9 and Beyond
- Memory, Garbage Collection and Performance
Module 11: Java Frameworks and Libraries
- Introduction to Java Frameworks
- Spring Framework
- Hibernate
- JUnit
- Maven
- Advanced Testing with Mockito
- Essential Ecosystem Libraries
