In the previous lesson you drew the blueprint: three classes, Book, Employee and Loan, with their fields and their responsibilities. Now it is time to build. This lesson is the one that turns the diagram into real Java code: you will learn the exact syntax to declare a class, what value its fields hold before anyone assigns them anything, how to create objects with new and what exactly happens in memory when you do. You will see why null is not "empty" but "no address at all", what the NullPointerException that will sooner or later hit you really means, and one of the nastiest surprises for beginners: two variables can point to the same object, so modifying it through one modifies it for the other too. By the end, "Effective Java", "Design Patterns" and "Refactoring" will exist in your program as three objects with a life of their own.

Contents

  1. Declaring a class: minimal syntax
  2. Instance fields and default values
  3. One file per public class and organisation into packages
  4. Creating objects with new
  5. What happens in memory: stack and heap
  6. Accessing fields with the dot
  7. The null reference and the NullPointerException
  8. Aliasing: several references to the same object
  9. static fields versus instance fields
  10. static final constants
  11. An object's life cycle and garbage collection
  12. BiblioTech: first working version of Book
  13. Common Mistakes and Tips
  14. Exercises

  1. Declaring a class: minimal syntax

A class is, in its simplest form, a name and a block:

public class Book {
    // the fields and methods go in here
}

Let us dissect it as calmly as you dissected main in module 1:

  • public is an access modifier: it says the class is visible from any package. If you omit it, the class will only be visible inside its own package (default, or "package-private", visibility). Modifiers are studied in depth in lesson 03-07.
  • class is the reserved word that declares a class.
  • Book is the name. By convention (module 1): a noun, singular, in UpperCamelCase.
  • The braces delimit the body of the class, where fields, constructors and methods live.

Watch out for a fundamental difference from everything you have written so far: inside a class body you cannot put loose statements. This does not compile:

public class Book {
    System.out.println("Hello");   // ERROR: statement outside a method
}

A class body only accepts declarations: fields, constructors, methods, initialiser blocks and other classes. Executable code lives inside methods.

  1. Instance fields and default values

A field (or attribute, or instance variable) is declared just like a local variable, but directly in the class body:

package com.nexussoftware.bibliotech.domain;

public class Book {
    public String  title;
    public String  author;
    public String  isbn;
    public int     publicationYear;
    public boolean available;
}

Important note: these fields are public only provisionally, so that you can manipulate them with the dot while you learn the mechanism. In lesson 03-07 you will make them private and expose them through controlled operations, which is how professional code is written. Consider this public a piece of scaffolding.

And here comes the first important difference from the local variables you already know. In module 1 you learned that a local variable must be initialised before use or the compiler complains. Fields, on the other hand, are automatically initialised with a default value:

Field type Default value Comment
byte, short, int, long 0 0L in the case of long
float, double 0.0
char ' ' The null character, not visible when printed
boolean false
Any reference (String, Book...) null Not the empty string: literally "no address"
Book empty = new Book();
System.out.println(empty.title);            // null   (not "" nor "empty")
System.out.println(empty.publicationYear);  // 0
System.out.println(empty.available);        // false

That automatic initialisation is a convenience, but also a trap: an object freshly created with new Book() is a book with no title, no author and not available, that is, an object in an invalid state. The definitive solution is constructors, which you will study in lesson 03-04 and which guarantee that no object is born incomplete. For now, we will assign the fields by hand.

  1. One file per public class and organisation into packages

Rules of the language, not of style:

  • A .java file can contain at most one public class.
  • If it contains a public class, the file must have the same name as the class, with the same capitalisation: Book goes in Book.java.
  • The package declared on the first line must match the directory path (module 1).

Applied to the bibliotech project, the structure looks like this:

bibliotech/
└── src/main/java/
    └── com/nexussoftware/bibliotech/
        ├── BiblioTechApp.java              (package com.nexussoftware.bibliotech)
        └── domain/
            ├── Book.java                    (package ...bibliotech.domain)
            ├── Employee.java
            └── Loan.java

The domain subpackage is not a whim: it separates the classes that represent the business from the code that runs the application. It is the course's first architectural decision, and you will pick it up again in 03-07 (package-level encapsulation) and in module 12.

Since BiblioTechApp and Book are now in different packages, your startup class needs an import:

package com.nexussoftware.bibliotech;

import com.nexussoftware.bibliotech.domain.Book;

public class BiblioTechApp {
    public static void main(String[] args) {
        // ...
    }
}

It is exactly the same mechanism you used with import java.util.Scanner;. The difference is that now you wrote the imported class yourself.

  1. Creating objects with new

The class is the blueprint; new is the builder that puts up the house:

Book effectiveJava = new Book();

Three parts worth separating mentally:

Fragment What it is What it does
Book effectiveJava Declaration of a reference-type variable Reserves a slot on the stack to hold an address
new Book() Creation operator Reserves memory on the heap and initialises the fields to their default values
= Assignment Copies the address of the freshly created object into the variable

The parentheses in new Book() are a call to the constructor. Since you have not written one yet, Java provides you with a default constructor with no parameters that does nothing beyond creating the object. In 03-04 you will see when that gift disappears.

Nothing forces you to keep the reference:

new Book();   // compiles: creates an object and loses it immediately

That object exists for an instant and becomes unreachable: there is no variable leading to it, so the garbage collector will eventually remove it (section 11).

  1. What happens in memory: stack and heap

Recall from module 1 the distinction between the stack and the heap:

  • The stack holds the local variables and parameters of each method being executed. It is small, extremely fast and frees itself when the method ends.
  • The heap holds all the objects. It is large, managed by the JVM and cleaned up by the garbage collector.

Run this fragment mentally:

Book a = new Book();
a.title = "Effective Java";
a.isbn  = "978-0000000001";

Book b = new Book();
b.title = "Design Patterns";
b.isbn  = "978-0000000002";
flowchart LR
    subgraph STACK["Stack (main)"]
        A["a = 0x1A"]
        B["b = 0x2B"]
    end
    subgraph HEAP["Heap"]
        O1["0x1A : Book
        title = Effective Java
        isbn = 978-0000000001
        available = false"]
        O2["0x2B : Book
        title = Design Patterns
        isbn = 978-0000000002
        available = false"]
    end
    A --> O1
    B --> O2

Four readings you should take from the diagram:

  1. The variable does not contain the object, it contains an arrow to it. The addresses (0x1A) are fictional; you will never see them nor need them.
  2. Each new produces a different object, even if the values were identical.
  3. The fields live inside the object, on the heap, not on the stack.
  4. When main ends, a and b disappear from the stack; the heap objects are left without references and become garbage.

  1. Accessing fields with the dot

The . operator is used to reach inside an object through a reference:

Book effectiveJava = new Book();

// Writing
effectiveJava.title           = "Effective Java";
effectiveJava.author          = "Joshua Bloch";
effectiveJava.isbn            = "978-0000000001";
effectiveJava.publicationYear = 2018;
effectiveJava.available       = true;

// Reading
System.out.println(effectiveJava.title);          // Effective Java
System.out.printf("%s (%d)%n", effectiveJava.author, effectiveJava.publicationYear);

The dot reads like a possessive: effectiveJava.title is "the title of effectiveJava". And it can be chained when a field is itself an object:

loan.book.title   // the title of the book of the loan

It works, but make a mental note that chaining dots is a design smell called a message train; lesson 03-07 formalises it with the Law of Demeter.

Fields can also be used in any of the expressions you already master:

if (effectiveJava.available && effectiveJava.publicationYear >= 2015) {
    System.out.println("Available and recent");
}

  1. The null reference and the NullPointerException

null is a special value meaning "this reference does not point to any object". It is not zero, it is not the empty string, it is not a "blank" object: it is the absence of an address.

Book unassigned = null;
System.out.println(unassigned);          // null

Printing a null reference is harmless. What is not harmless is trying to use the dot on it:

Book unassigned = null;
System.out.println(unassigned.title);    // NullPointerException at run time

The program stops with a trace like this:

Exception in thread "main" java.lang.NullPointerException:
        Cannot read field "title" because "unassigned" is null
        at com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:14)

Read it with the systematic method from lesson 02-05:

Part of the message What it tells you
NullPointerException The dot has been used on a null reference
Cannot read field "title" The field that was being accessed
because "unassigned" is null Which reference was null (detailed message from Java 14+)
at ...BiblioTechApp.main(BiblioTechApp.java:14) Exact file and line

That "because ... is null" is a huge improvement in modern Java: in older versions you only saw the exception name and the line, and with a.b.c.d you had to guess which of the four was null.

A NullPointerException is not solved with try/catch (module 6): it is solved by preventing the null reference from existing. The tools for that are the null guards you already used in module 2 and, above all, the constructors that force you to supply values (03-04):

if (book != null) {
    System.out.println(book.title);
} else {
    System.out.println("There is no book associated.");
}

Remember the string-comparison rule from module 1: with a possible null in the mix, "Effective Java".equals(book.title) is safer than book.title.equals("Effective Java"), because the literal is never null.

  1. Aliasing: several references to the same object

This is the concept that causes the most surprises. Look:

Book original = new Book();
original.title     = "Refactoring";
original.available = true;

Book copy = original;      // a copy?
copy.available = false;    // "copy" is modified

System.out.println(original.available);   // false  <-- it was not a copy!

Book copy = original; does not duplicate the object. It copies the reference, that is, the address. The result is two variables pointing at the same object on the heap:

flowchart LR
    subgraph STACK["Stack"]
        V1["original = 0x3C"]
        V2["copy = 0x3C"]
    end
    subgraph HEAP["Heap"]
        OBJ["0x3C : Book
        title = Refactoring
        available = false"]
    end
    V1 --> OBJ
    V2 --> OBJ

This phenomenon is called aliasing (two names for the same thing). Practical consequences:

Operation Effect
copy.available = false; Modifies the object; it is seen from original
copy = new Book(); Repoints the variable copy; original is unaffected
copy == original true if they point to the same object (identity)
copy = null; Only copy becomes null; the object stays alive through original

Check the difference between mutating and repointing:

Book a = new Book();
a.title = "Effective Java";

Book b = a;
b.title = "Design Patterns";          // MUTATE the shared object
System.out.println(a.title);          // Design Patterns

b = new Book();                       // REPOINT the variable b
b.title = "Refactoring";
System.out.println(a.title);          // Design Patterns  (a does not change)

And compare identity with equality, picking up == from module 1:

Book x = new Book();
x.isbn = "978-0000000001";

Book y = new Book();
y.isbn = "978-0000000001";

System.out.println(x == y);   // false: they are two different objects
System.out.println(x == x);   // true

Two books with the same ISBN are different objects and == says so plainly. And if you want them to be considered equal by their ISBN? That requires overriding equals, and it is the subject of lesson 03-09.

If you really want to duplicate an object, you have to do it field by field (or with a copy constructor, lesson 03-04):

Book duplicate = new Book();
duplicate.title           = original.title;
duplicate.author          = original.author;
duplicate.isbn            = original.isbn;
duplicate.publicationYear = original.publicationYear;
duplicate.available       = original.available;

  1. static fields versus instance fields

All the fields seen so far are instance fields: each object has its own copy. A static field is different: it belongs to the class, and there is a single copy shared by all objects.

The classic use case is a counter. Nexus Software wants to know how many loans have been created during the session:

package com.nexussoftware.bibliotech.domain;

public class Loan {

    // Class field: only one, shared by all loans
    public static int loansCreated = 0;

    // Instance fields: one per object
    public Book     book;
    public Employee employee;
    public int      elapsedDays;
}

For now the counter has to be incremented by hand when each loan is created (in 03-04 the constructor will do it, which is its natural home):

Loan l1 = new Loan();
Loan.loansCreated++;

Loan l2 = new Loan();
Loan.loansCreated++;

System.out.println(Loan.loansCreated);   // 2

Essential differences:

Aspect Instance field static field
How many copies there are One per object One per class
How it is accessed object.field Class.field (recommended)
When it exists From new until it is collected From when the class is loaded until the program ends
Example book.title Loan.loansCreated
Used for State belonging to each object State or data common to all

Java lets you access a static field through an instance (l1.loansCreated), but it is very bad practice: it gives the false impression that the value belongs to that object. Always use the class name.

And a warning: mutable static fields are global state in disguise, with the same risks as the twenty variables in main. Use them for counters, constants and little else.

  1. static final constants

Combining static (a single copy) with final (cannot be reassigned, module 1) gives the canonical way to declare a constant:

public class Loan {

    /** Standard duration of a loan, in days. */
    public static final int LOAN_DAYS = 15;

    /** Amount charged for each day late, in euros. */
    public static final double DAILY_RATE = 0.25;

    /** Maximum amount a fine can reach, in euros. */
    public static final double MAX_FINE = 20.0;

    /** Maximum delay (in days) considered minor. */
    public static final int MINOR_THRESHOLD = 7;

    public static int loansCreated = 0;

    public Book     book;
    public Employee employee;
    public int      elapsedDays;
}

This solves a real problem in your current code: until now the four business rules were final locals inside main, invisible to any other part of the program. Now they live in the class that governs them and are used from anywhere:

System.out.printf("Term: %d days, rate %.2f EUR/day%n",
                  Loan.LOAN_DAYS, Loan.DAILY_RATE);

Conventions for constants (module 1): UPPERCASE_WITH_UNDERSCORES. And a caution: final prevents reassigning the variable, not modifying the object it points to. public static final Book BASE_CATALOG = new Book(); would stop you doing BASE_CATALOG = anotherBook;, but not BASE_CATALOG.title = "Another";. That is why constants are declared with immutable types (primitives, String).

  1. An object's life cycle and garbage collection

A Java object goes through three phases:

flowchart LR
    A["1. Creation
    new reserves memory
    and initialises fields"] --> B["2. Use
    there is at least
    one live reference"]
    B --> C["3. Unreachable
    no reference
    leads to it"]
    C --> D["4. Collected
    the GC frees
    the memory"]

In Java there is no operator to destroy objects (there is no delete as in C++). When an object stops being reachable from any live reference, it becomes a candidate for removal by the garbage collector (GC), a JVM process that frees memory automatically.

Book temp = new Book();
temp.title = "Refactoring";

temp = null;   // the object becomes unreachable: it will be collected

Three things you should already know, without going into detail:

  • You do not control when the GC runs. It may be immediately or much later. System.gc() is a suggestion the JVM may ignore; do not use it.
  • The GC eliminates most memory leaks, but not all: if you keep live references to objects you no longer need (for example, in a global structure that grows without limit), the memory is not freed.
  • Do not put cleanup logic in finalize(): it has been deprecated since Java 9 and must not be used (lesson 03-09).

The GC's internal workings, the generations and performance tuning are studied in lesson 10-07.

  1. BiblioTech: first working version of Book

Let us put it all together. File src/main/java/com/nexussoftware/bibliotech/domain/Book.java:

package com.nexussoftware.bibliotech.domain;

/**
 * Represents a book in Nexus Software's technical catalog.
 *
 * Version 1: the fields are public provisionally so they can be
 * assigned with the dot operator. In lesson 03-07 they will become
 * private and be reached through controlled operations.
 */
public class Book {

    /** Total number of books created during the run. */
    public static int booksCreated = 0;

    public String  title;
    public String  author;
    public String  isbn;
    public int     publicationYear;
    public boolean available;
}

File Loan.java, still minimal (its operations arrive in 03-03 and its constructor in 03-04):

package com.nexussoftware.bibliotech.domain;

/** Record of a loan of a book to an employee. */
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;

    public static int loansCreated = 0;

    public Book   book;
    public String employee;          // will become an Employee object in 03-04
    public int    elapsedDays;
}

And the entry point, BiblioTechApp.java:

package com.nexussoftware.bibliotech;

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.0 - Object catalog ===");

        // --- Creation of the three catalog books ---
        Book effectiveJava = new Book();
        effectiveJava.title           = "Effective Java";
        effectiveJava.author          = "Joshua Bloch";
        effectiveJava.isbn            = "978-0000000001";
        effectiveJava.publicationYear = 2018;
        effectiveJava.available       = true;
        Book.booksCreated++;

        Book designPatterns = new Book();
        designPatterns.title           = "Design Patterns";
        designPatterns.author          = "Erich Gamma";
        designPatterns.isbn            = "978-0000000002";
        designPatterns.publicationYear = 1994;
        designPatterns.available       = true;
        Book.booksCreated++;

        Book refactoring = new Book();
        refactoring.title           = "Refactoring";
        refactoring.author          = "Martin Fowler";
        refactoring.isbn            = "978-0000000003";
        refactoring.publicationYear = 2018;
        refactoring.available       = false;
        Book.booksCreated++;

        // --- Listing ---
        System.out.println("\nCatalog:");
        System.out.printf("  %-22s %-16s %-16s %4s  %s%n",
                          "TITLE", "AUTHOR", "ISBN", "YEAR", "STATUS");
        print(effectiveJava);
        print(designPatterns);
        print(refactoring);

        System.out.printf("%nBooks created: %d%n", Book.booksCreated);

        // --- A loan referencing one of those books ---
        Loan l = new Loan();
        l.book        = refactoring;   // reference to the SAME object
        l.employee    = "Marta Ruiz";
        l.elapsedDays = 20;
        Loan.loansCreated++;

        System.out.printf("%nLoan of '%s' to %s (%d days elapsed)%n",
                          l.book.title, l.employee, l.elapsedDays);
        System.out.printf("Rules in force: %d days term, %.2f EUR/day, cap %.2f EUR%n",
                          Loan.LOAN_DAYS, Loan.DAILY_RATE,
                          Loan.MAX_FINE);
    }

    /** Prints one catalog row. Helper method; methods are explained in 03-03. */
    private static void print(Book book) {
        String status = book.available ? "Available" : "On loan";
        System.out.printf("  %-22s %-16s %-16s %4d  %s%n",
                          book.title, book.author, book.isbn,
                          book.publicationYear, status);
    }
}

Output:

=== BiblioTech 3.0 - Object catalog ===

Catalog:
  TITLE                  AUTHOR           ISBN             YEAR  STATUS
  Effective Java         Joshua Bloch     978-0000000001   2018  Available
  Design Patterns        Erich Gamma      978-0000000002   1994  Available
  Refactoring            Martin Fowler    978-0000000003   2018  On loan

Books created: 3

Loan of 'Refactoring' to Marta Ruiz (20 days elapsed)
Rules in force: 15 days term, 0.25 EUR/day, cap 20.00 EUR

Compare this code with module 2's. A book's data is no longer scattered: it travels together inside an object, and you can pass the whole book to a method with a single parameter (print(effectiveJava)) instead of five. And l.book = refactoring; is intentional aliasing: the loan points to the same copy in the catalog, so when in the next lesson you set l.book.available = false, the catalog will find out by itself.

What is still missing is obvious: objects are born empty and have to be filled in by hand field by field (constructors, 03-04), they do not know how to do anything (methods, 03-03), and with more than three books the code becomes unworkable because there is nowhere to keep them: that is what module 5 solves with collections.

Common Mistakes and Tips

  • Forgetting the new. Book book; declares a reference, it does not create anything. If you use it, you get a NullPointerException (if it is a field) or a compile error for an uninitialised variable (if it is local).
  • Believing that = copies the object. This is the mistake from section 8. b = a; creates an alias, not a copy. If you need an independent object, you have to build it.
  • Confusing null with "" or with 0. null means "no object". An empty string is a perfectly valid String object on which you can call methods; on null you can call nothing.
  • Giving the file a different name from the public class. class Book in book.java does not compile on case-sensitive systems (every Linux, and the JVM in general). Let the IDE create the classes for you.
  • Declaring fields static "because it is easier to access them that way". It is the fast lane to global state shared by every object: if title were static, all three books would have the same title! A field is static only if its value is genuinely common to the whole class.
  • Accessing a static field through an instance. l1.loansCreated compiles, but misleads the reader. Always write Loan.loansCreated.
  • Tip: faced with a NullPointerException, do not look for where it failed, look for where the assignment was missing. The message "because X is null" gives you the exact name of the reference; work backwards to the point where it should have been assigned.
  • Tip: start getting used to your IDE's Alt+Insert / "Generate". It will generate constructors, getters and toString as soon as you study them. But write them by hand the first few times: only that way do you understand what it generates.

Exercises

Exercise 1: the Employee class

Create the Employee class in the com.nexussoftware.bibliotech.domain package with:

  • Public fields name (String), identifier (String) and totalLoans (int).
  • A static field employeesRegistered counting how many have been created.
  • A static final constant MAX_CONCURRENT_LOANS with value 3.

Then, in BiblioTechApp, create Nexus Software's three employees (Marta Ruiz with identifier EMP-001, Diego Alonso with EMP-002 and Nuria Vidal with EMP-003), give them 2, 0 and 4 accumulated loans respectively, print a table with printf and mark with an asterisk anyone exceeding the permitted maximum. Show the registered-employee counter at the end.

Exercise 2: demonstrate aliasing

Write a program that demonstrates, with console output, the four situations in the table of section 8, using two Book references:

  1. Mutate the object through one reference and see the change from the other.
  2. Repoint one reference to a new object and check that the other does not change.
  3. Compare with == two references to the same object and two different objects with an identical ISBN.
  4. Set one reference to null and check that the object is still reachable through the other.

Explain in comments what happens in memory at each step.

Exercise 3: trigger and read a NullPointerException

Write a fragment that deliberately triggers a NullPointerException on a Loan object whose book field has not been assigned. Copy the full trace you get, and identify in it the null reference, the guilty line and the field that was being accessed. Then fix the program without using try/catch, in two different ways: (a) with a null guard, and (b) by assigning the field before using it. Argue which of the two is the better solution and why.

Solutions

Solution 1

Employee.java:

package com.nexussoftware.bibliotech.domain;

/**
 * Nexus Software employee authorised to take books on loan.
 * Public fields provisionally (see lesson 03-07).
 */
public class Employee {

    /** Maximum number of concurrent loans allowed by the internal policy. */
    public static final int MAX_CONCURRENT_LOANS = 3;

    /** How many employees have been registered in this run. */
    public static int employeesRegistered = 0;

    public String name;
    public String identifier;
    public int    totalLoans;
}

Use in BiblioTechApp:

Employee marta = new Employee();
marta.name       = "Marta Ruiz";
marta.identifier = "EMP-001";
marta.totalLoans = 2;
Employee.employeesRegistered++;

Employee diego = new Employee();
diego.name       = "Diego Alonso";
diego.identifier = "EMP-002";
diego.totalLoans = 0;
Employee.employeesRegistered++;

Employee nuria = new Employee();
nuria.name       = "Nuria Vidal";
nuria.identifier = "EMP-003";
nuria.totalLoans = 4;
Employee.employeesRegistered++;

System.out.printf("%n%-14s %-9s %-10s %s%n", "EMPLOYEE", "ID", "LOANS", "NOTICE");
printEmployee(marta);
printEmployee(diego);
printEmployee(nuria);

System.out.printf("%nEmployees registered: %d (maximum %d loans per person)%n",
                  Employee.employeesRegistered,
                  Employee.MAX_CONCURRENT_LOANS);
private static void printEmployee(Employee e) {
    // The asterisk marks anyone exceeding the internal policy limit.
    String notice = e.totalLoans > Employee.MAX_CONCURRENT_LOANS ? "*" : "";
    System.out.printf("%-14s %-9s %-10d %s%n",
                      e.name, e.identifier, e.totalLoans, notice);
}

Output:

EMPLOYEE       ID        LOANS      NOTICE
Marta Ruiz     EMP-001   2
Diego Alonso   EMP-002   0
Nuria Vidal    EMP-003   4          *

Employees registered: 3 (maximum 3 loans per person)

Key points: MAX_CONCURRENT_LOANS is static final because the policy belongs to the company, not to each person; employeesRegistered is static (one for everyone) while totalLoans is an instance field (one per employee). If you confused the two, all three employees would share the same number of loans, which is precisely the mistake in the tips section.

Solution 2

package com.nexussoftware.bibliotech;

import com.nexussoftware.bibliotech.domain.Book;

public class AliasingDemo {

    public static void main(String[] args) {

        // --- Situation 1: mutate through an alias ---
        Book a = new Book();
        a.title     = "Effective Java";
        a.isbn      = "978-0000000001";
        a.available = true;

        Book b = a;              // b and a point to the SAME heap object
        b.available = false;     // the shared object is mutated

        System.out.println("1) a.available = " + a.available);   // false
        System.out.println("   b.available = " + b.available);   // false
        // In memory: a single Book box with two arrows pointing at it.

        // --- Situation 2: repoint a reference ---
        b = new Book();          // b now points to a NEW object
        b.title = "Design Patterns";
        b.isbn  = "978-0000000002";

        System.out.println("2) a.title = " + a.title);   // Effective Java
        System.out.println("   b.title = " + b.title);   // Design Patterns
        // In memory: two different boxes; b's arrow has moved,
        // a's has not been touched.

        // --- Situation 3: identity with == ---
        Book c = a;              // alias of a
        Book d = new Book();
        d.isbn = a.isbn;         // same ISBN, different object

        System.out.println("3) a == c : " + (a == c));   // true
        System.out.println("   a == d : " + (a == d));   // false
        System.out.println("   same ISBN: " + a.isbn.equals(d.isbn));   // true
        // == compares addresses, not contents: a and d are two different
        // copies even though they describe the same title. For them to be
        // considered equal by ISBN you must override equals (lesson 03-09).

        // --- Situation 4: nulling a reference ---
        c = null;                // only c's arrow is erased

        System.out.println("4) c is null: " + (c == null));       // true
        System.out.println("   a.title is still: " + a.title);    // Effective Java
        // The object is not collected because a still points to it.
    }
}

Output:

1) a.available = false
   b.available = false
2) a.title = Effective Java
   b.title = Design Patterns
3) a == c : true
   a == d : false
   same ISBN: true
4) c is null: true
   a.title is still: Effective Java

The rule to memorise: the dot mutates the object and it is seen from every alias; the = moves the arrow of a single variable.

Solution 3

Code that triggers the failure:

Loan l = new Loan();
l.employee    = "Diego Alonso";
l.elapsedDays = 18;
// l.book has NOT been assigned: it is null by default

System.out.println("Book on loan: " + l.book.title);   // line 15

Trace obtained:

Exception in thread "main" java.lang.NullPointerException:
        Cannot read field "title" because "l.book" is null
        at com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:15)

Reading the trace:

Item Value
Null reference l.book (not l, which is created)
Field accessed title
Guilty line 15 of BiblioTechApp.java
Root cause The book field was never assigned, so it keeps its default value null

(a) Null guard, with the pattern from module 2:

if (l.book != null) {
    System.out.println("Book on loan: " + l.book.title);
} else {
    System.out.println("Loan with no associated book (incomplete record).");
}

(b) Assign the field before using it:

Loan l = new Loan();
l.book        = effectiveJava;   // now the loan has a book
l.employee    = "Diego Alonso";
l.elapsedDays = 18;

System.out.println("Book on loan: " + l.book.title);   // works

Which is better? (b), without any doubt. Option (a) treats the symptom: it accepts that loans without a book may exist and adds a check that would have to be repeated at every point where l.book is used. Option (b) attacks the cause: a loan without a book is not a loan, it is an object in an invalid state that should never have existed.

The definitive version of that idea arrives in lesson 03-04: a constructor that demands the book as a parameter makes it impossible to create an incomplete Loan, and then the null guard is redundant because the case cannot arise. That is the difference between defensive programming and good design: the best NullPointerException is the one that cannot happen.

Conclusion

You have made the jump from drawing classes to writing them. You know how to declare a class and its fields, and you know the —often forgotten— rule that fields initialise themselves with 0, false or null, which means a freshly created object can be perfectly empty and perfectly invalid. You have seen exactly what new does: reserve memory on the heap, initialise the fields and return a reference that you store on the stack; and you have traced that distribution of memory in diagrams. You handle the dot operator to read and write fields, you understand null as the absence of an address and you know how to read a NullPointerException down to identifying which reference failed and why. You have grasped aliasing, which is the source of a huge proportion of beginners' errors: b = a does not copy, it shares. You tell instance fields from static fields, you have moved BiblioTech's four business rules into static final constants inside Loan, where they finally belong, and you know that memory is freed by the garbage collector with no intervention from you. Your catalog is now objects: Book, Employee and a Loan that relates them.

But your objects still do not know how to do anything: they are boxes of data that somebody manipulates from outside. The living half of OOP is missing. In the next lesson, Methods, you will give behaviour to your classes: you will learn the full anatomy of a method, discover why in Java everything is passed by value —and what that surprisingly implies when the argument is an object—, master overloading, this, varargs and the difference between static and instance methods. And you will carry out the module's first big refactoring: taking the calculations of days late, fine and severity out of main, and moving them into Loan, their rightful owner.

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