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

  1. What a constructor is
  2. Constructor versus method: seven differences
  3. The default constructor and when it disappears
  4. Constructor overloading
  5. Chaining with this(...) and its rules
  6. Order of initialisation, step by step
  7. final fields and their mandatory initialisation
  8. The telescoping constructor and the Builder pattern
  9. Validating in the constructor
  10. The copy constructor
  11. static initialisation blocks
  12. BiblioTech: Book, Employee and Loan with complete constructors
  13. Common Mistakes and Tips
  14. Exercises

  1. 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:

Book effectiveJava = new Book("Effective Java", "Joshua Bloch", "978-0000000001");

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.

  1. 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:

public class Book {
    public void Book(String title) {     // NOT a constructor!
        this.title = title;
    }
}

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.

  1. 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.

public class Book { }          // Java adds: public Book() { }

Book b = new Book();           // works

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 ERROR
error: 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:

public Book() {
    this("Untitled", "Unknown", "000-0000000000");
}

  1. 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");

  1. Chaining with this(...) and its rules

In 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:

  1. this(...) must be the first statement of the constructor. Not even a System.out.println before it.
  2. You cannot have both this(...) and super(...) (lesson 03-05): only one of the two, and always first.
  3. Cycles are not allowed: if A() calls B() and B() calls A(), 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);
}
error: call to this must be first statement in constructor

  1. 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:

  1. Memory is reserved and all the fields take their default value (0, false, null).
  2. The field initialisers and the instance initialisation blocks run, in the textual order in which they appear in the file.
  3. 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.

  1. final fields and their mandatory initialisation

You 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.

  1. 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 —title and isbn, both String—, 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.

  1. 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.

  1. 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 objects

Compare 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.

  1. static initialisation blocks

Just 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:

[Loan] Rules loaded. Cap at 80 days late.

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.

  1. BiblioTech: Book, Employee and Loan with complete constructors

Let 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: loanDay is the day in BiblioTech's internal calendar on which the book was handed over (for example, operating day 100). Real dates with LocalDate arrive 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:

  • dueDay is 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.
  • reference generates itself from the static counter, with String.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 use new.
  • 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 only private or final methods.
  • Assigning without this when 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 final every 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:

  1. A canonical constructor that validates: the hours within 0-23, endHour later than startHour and attendees greater than zero. Apply safe default values and warn on the console.
  2. An overloaded constructor that assumes a duration of one hour (it receives only the start hour).
  3. A static counter of created reservations that feeds the code.
  4. 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:

  1. The duplicate and the original are different objects (== gives false).
  2. Changing the availability of one does not affect the other.
  3. 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:

  1. Deep copy: also create a new Employee in the copy constructor (this.lastReader = new Employee(other.lastReader);). Expensive, and it duplicates entities that perhaps ought to be unique.
  2. Share on purpose: if the Employee is 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.
  3. 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

Module 2: Control Flow

Module 3: Object-Oriented Programming

Module 4: Advanced Object-Oriented Programming

Module 5: Data Structures and Collections

Module 6: Exception Handling

Module 7: File Input/Output

Module 8: Multithreading and Concurrency

Module 9: Networking

Module 10: Advanced Topics

Module 11: Java Frameworks and Libraries

Module 12: Building Real-World Applications

© Copyright 2026. All rights reserved