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
- Declaring a class: minimal syntax
- Instance fields and default values
- One file per public class and organisation into packages
- Creating objects with
new - What happens in memory: stack and heap
- Accessing fields with the dot
- The
nullreference and theNullPointerException - Aliasing: several references to the same object
staticfields versus instance fieldsstatic finalconstants- An object's life cycle and garbage collection
- BiblioTech: first working version of
Book - Common Mistakes and Tips
- Exercises
- Declaring a class: minimal syntax
A class is, in its simplest form, a name and a block:
Let us dissect it as calmly as you dissected main in module 1:
publicis 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.classis the reserved word that declares a class.Bookis the name. By convention (module 1): a noun, singular, inUpperCamelCase.- 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:
A class body only accepts declarations: fields, constructors, methods, initialiser blocks and other classes. Executable code lives inside methods.
- 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
publiconly provisionally, so that you can manipulate them with the dot while you learn the mechanism. In lesson 03-07 you will make themprivateand expose them through controlled operations, which is how professional code is written. Consider thispublica 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); // falseThat 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.
- One file per public class and organisation into packages
Rules of the language, not of style:
- A
.javafile can contain at most onepublicclass. - If it contains a
publicclass, the file must have the same name as the class, with the same capitalisation:Bookgoes inBook.java. - The
packagedeclared 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.javaThe 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.
- Creating objects with
new
newThe class is the blueprint; new is the builder that puts up the house:
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:
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).
- 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:
- 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. - Each
newproduces a different object, even if the values were identical. - The fields live inside the object, on the heap, not on the stack.
- When
mainends,aandbdisappear from the stack; the heap objects are left without references and become garbage.
- 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:
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");
}
- The
null reference and the NullPointerException
null reference and the NullPointerExceptionnull 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.
Printing a null reference is harmless. What is not harmless is trying to use the dot on it:
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.
- 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); // trueTwo 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;
static fields versus instance fields
static fields versus instance fieldsAll 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); // 2Essential 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.
static final constants
static final constantsCombining 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:
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).
- 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 collectedThree 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.
- BiblioTech: first working version of
Book
BookLet 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 aNullPointerException(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
nullwith""or with0.nullmeans "no object". An empty string is a perfectly validStringobject on which you can call methods; onnullyou can call nothing. - Giving the file a different name from the public class.
class Bookinbook.javadoes 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: iftitlewerestatic, all three books would have the same title! A field isstaticonly if its value is genuinely common to the whole class. - Accessing a
staticfield through an instance.l1.loansCreatedcompiles, but misleads the reader. Always writeLoan.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 andtoStringas 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) andtotalLoans(int). - A
staticfieldemployeesRegisteredcounting how many have been created. - A
static finalconstantMAX_CONCURRENT_LOANSwith 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:
- Mutate the object through one reference and see the change from the other.
- Repoint one reference to a new object and check that the other does not change.
- Compare with
==two references to the same object and two different objects with an identical ISBN. - Set one reference to
nulland 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 15Trace 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); // worksWhich 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
- 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
