# NOTES — one book, two holders, and the seven things that follow

## Run it first

Four files, from the directory holding the sources:

```
.toolchain\jdk-21\bin\javac.exe -Xlint:all -d out *.java
.toolchain\jdk-21\bin\java.exe -cp out Main
```

`-Xlint:all` prints nothing. Note that now, because block 5 is a null dereference the compiler
had every chance to see.

Real output, verbatim:

```
--- 1. two names, one Copy
onTheShelf.damaged()   : true
onTheShelf == sameBook : true
onTheShelf             : Copy[978-0132350884#1, damaged]

--- 2. passing a Copy to a method
before             : Copy[978-0132350884#2, intact]
after inspect()    : Copy[978-0132350884#2, damaged]
barcode unchanged  : 978-0132350884#2

--- 3. one Copy, referenced from the shelf and from the card
card              : LoanCard[978-0132350884#1 -> M-1, due 2026-01-26T09:00:00Z]
shelf's object == card's object : true
shelf's view now  : Copy[978-0132350884#1, damaged]
card's view now   : Copy[978-0132350884#1, damaged]

--- 4. List.copyOf protects the list, not the copies
handedOut.add(...)     : UnsupportedOperationException
shelf's second copy    : Copy[978-0132350884#2, damaged]

--- 5. null, and the two shapes of the message
copyWithBarcode(unheld): null
through a local        : Cannot invoke "Copy.barcode()" because "<local1>" is null
through a call         : Cannot invoke "Copy.barcode()" because the return value of "Shelf.copyWithBarcode(String)" is null

--- 6. a reference cycle, unreachable
collected after gc     : true

--- 7. the leak garbage collection cannot prevent
collected while out    : false
collected after takeBack : true
copies out, both shelves : 0 and 0
collected after takeBackLeavingTheCard : false
```

Every line is deterministic across runs, including the three booleans in block 7. The one thing
that varies is `<local1>` in block 5, which becomes `"missing"` under `javac -g`.

**The two lines to read together are the last two of block 7.** Both shelves say zero copies
out, so both look correct from the desk. One of them is holding a finished loan it will never
let go of.

---

## `Copy.java` — the mutable object the whole lesson turns on

```java
public final class Copy {

    private final String barcode;
    private final String isbn;
    private boolean damaged;
```

**`damaged` being mutable is the deliberate choice, and it is what makes the rest of the file
interesting.** The corpus's own value types are not like this: `Title` and `Loan` are records,
and `Member` is a final class whose `owing()` returns a new instance. With an immutable type,
everything below stops being a question, because a reference you cannot change through is as
good as a copy. This lesson needs a mutable object because the traps only exist around one.

**`final` on the two `String` fields buys less than `const` would.** It stops the field being
reassigned. It says nothing about what the referenced object can do. `String` happens to be
immutable, so here the two coincide; write `private final List<String> notes` and any holder of
the reference can still call `add`. That is the trap J1 flagged and this file is where it bites.

```java
    public void markDamaged() {
        this.damaged = true;
    }
```

**One public mutator is all it takes to make the sharing in block 3 a design question.** Delete
this method and `Copy` becomes immutable, `Shelf.copies()` needs no defensive thought, and the
loan card can hand its copy to anyone. The count of mutators is the size of the problem.

```java
    private LoanCard onLoan;
```

**The back-reference that completes the cycle, and the one that leaks in block 7.** `Copy`
points at `LoanCard`, `LoanCard` points at `Copy`. In C++ with `shared_ptr` on both sides that
pair is never freed, and the fix is a `weak_ptr` on one side. Here it costs nothing while both
are unreachable, and costs everything if a live shelf is at the other end of it.

```java
    void lentOn(LoanCard card) {
        this.onLoan = Objects.requireNonNull(card, "card");
    }
```

**Package-private, so the lifecycle has one driver.** `Shelf` is the only thing that lends and
takes back, so it is the only thing that may set this. If `lentOn` were public, any holder of a
`Copy` could point it at a card the shelf has never heard of, and `takeBack` would then clear a
reference the shelf did not create. Narrow visibility is how you say "this transition is mine"
in a language with no `friend`.

**`requireNonNull` with the parameter name, not a bare assignment.** Without it, a null card is
stored happily and the failure arrives later, at whichever `.` reads it. The exception message
would then name a field or a `<localN>`, several frames away from the caller who passed null.
With it, the stack trace's top frame is the mistake.

---

## `LoanCard.java` — the field that would have been five decisions

```java
public final class LoanCard {

    private final Copy copy;
```

**In C++ this declaration is where you commit to a lifetime story.** `Copy`, `Copy&`, `Copy*`,
`unique_ptr<Copy>`, `shared_ptr<Copy>` all compile and all mean different things about who
frees the book and who may outlive whom. Java has one spelling, so this line carries no
lifetime claim at all. Nothing here says the shelf also holds this book, and nothing needs to.

**What this line does still claim:** the card cannot be re-pointed at a different book, because
of `final`. It does not claim the book cannot change. `copy.markDamaged()` from inside this
class would compile.

```java
    public Copy copy() {
        return copy;
    }
```

**This accessor hands out the shelf's object.** That is the aliasing in block 3, and it is a
decision rather than an accident. Three alternatives, in order of cost. Return the barcode
instead of the book. Return an immutable snapshot. Or keep this line and accept that the card's
holder may damage a book on the shelf.

The corpus took the first one. `corpus/library/contract/Loan.java` records a `String copyId`,
not a `Copy`, and its own comment says a loan is "a snapshot of a loan as it stood when it was
handed to you". A loan holding an id has no aliasing question to answer, and it also cannot go
stale. That is the design payoff of this lesson, and it is why the reference solution never
needs a defensive copy.

**What breaks if you keep the reference anyway:** nothing, until a second holder mutates. Then
you have two correct-looking classes and a bug that belongs to neither.

---

## `Shelf.java` — where reachability becomes your problem

```java
    private final List<Copy> copies = new ArrayList<>();
    private final Map<String, LoanCard> out = new HashMap<>();
```

**These two fields are the lifetime of every book in the branch.** A `Shelf` that lives for the
process holds its `Copy` objects for the process, which is correct. The `out` map is the one
that must shrink, because a loan is finished at some point and the shelf is not.

**Both fields are `private`, and that is load-bearing rather than habit.** They are the reason
`copies()` can decide what escapes. Make either one public and every rule below is unenforceable
from inside this class.

```java
    public void restock(List<Copy> arriving) {
        Objects.requireNonNull(arriving, "arriving");
        copies.addAll(arriving);
    }
```

**`addAll` copies the list, not the books.** The shelf's list and the caller's list are now two
lists holding the same `Copy` objects. Clearing the caller's list changes nothing here; damaging
a book through the caller's list is visible here immediately.

**The alternative that looks equivalent and is not.** `this.copies = arriving` would make the
caller's list *be* the shelf's list, so a later `arriving.clear()` empties the shelf from
outside. It also discards everything restocked earlier. Both failures are silent.

**`requireNonNull` before `addAll`, deliberately.** `copies.addAll(null)` throws anyway, but the
message comes from inside `ArrayList` and names an expression you did not write. Rejecting at
the boundary produces `NullPointerException: arriving`, which is the parameter and the caller's
mistake.

```java
    public List<Copy> copies() {
        return List.copyOf(copies);
    }
```

**This stops the shelf growing from outside, and nothing else.** `List.copyOf` returns an
unmodifiable list of the same references, so `add` throws and `get(1).markDamaged()` reaches the
shelf's own book. Block 4 shows both halves in two lines of output.

**Returning `copies` directly is the bug this line prevents:** any caller could `add` a book the
shelf never accessioned, or `clear` the branch. `Collections.unmodifiableList(copies)` is the
other option and is not the same thing. It is a live view, so the caller sees later restocks; a
caller iterating it while the shelf restocks gets `ConcurrentModificationException`.

The corpus uses this same call in `InMemoryCatalogue.titles()` and
`InMemoryLoanRepository.outstanding()`. There it is a complete defence, because the elements are
records. Here it is half of one.

```java
    public Copy copyWithBarcode(String barcode) {
        for (Copy copy : copies) {
            if (copy.barcode().equals(barcode)) {
                return copy;
            }
        }
        return null;
    }
```

**`return null` is the line that creates block 5.** The method's signature promises a `Copy` and
delivers a value that fails at the first `.`. No caller is forced to check, no compiler flags the
omission, and `-Xlint:all` says nothing.

Three ways to remove the trap, and the corpus uses two of them:

- `Optional<Copy>` as the return type, as in `Catalogue.find`. The call site cannot ignore the
  empty case without writing something that reads wrong.
- Throw for the absent case, as in `Shelf.require(isbn)` with `UnknownIsbnException`. Right when
  absence is a caller error rather than an expected answer.
- Return null and document it. Acceptable for a private helper whose two callers are on the same
  screen, which is what this one is used as by `lend`.

**`copy.barcode().equals(barcode)`, not `==`.** That is J6's trap, and it is here because
`copies()` returning `List<Copy>` means every lookup goes through a comparison somebody has to
get right.

```java
    public LoanCard lend(String barcode, String memberId, Instant dueAt) {
        Copy copy = copyWithBarcode(barcode);
        if (copy == null) {
            throw new IllegalArgumentException("no copy with barcode " + barcode);
        }
```

**The null check converts a later `NullPointerException` into a message with the barcode in it.**
Without it, `new LoanCard(copy, ...)` throws `NullPointerException: copy` from `requireNonNull`,
which tells you a null arrived and not which barcode was asked for. The rule that generalises:
check for absence where you still know what was being looked for.

```java
        LoanCard card = new LoanCard(copy, memberId, dueAt);
        out.put(barcode, card);
        copy.lentOn(card);
        return card;
    }
```

**Three references to one card are created here, and `takeBack` has to drop two of them.** The
map holds one, the copy holds one, the caller holds the returned one. The caller's goes away on
its own when the caller's frame does. The other two are the shelf's job.

```java
    public void takeBack(String barcode) {
        LoanCard card = requireOut(barcode);
        out.remove(barcode);
        card.copy().backOnShelf();
    }
```

**`out.remove` alone is not enough, and that is the whole point of block 7.** Removing the map
entry makes `copiesOut()` correct. The card is still reachable through `Copy.onLoan`, and the
copy is reachable from `copies` for the life of the shelf, so the finished loan is retained.
`backOnShelf()` is the line that ends it.

**What this costs if omitted:** one `LoanCard` and one `Instant` retained per return, forever,
with every number at the desk still correct. Nothing throws. A heap dump shows growth in
`LoanCard`, and the only way to find the cause is to ask which reference chain still reaches it.

**`takeBackLeavingTheCard` exists so the difference is one call rather than one paragraph.** It
is the same method with `backOnShelf()` deleted. Compare the two output lines it produces.

---

## `Main.java` — two details that make the measurements honest

```java
    private static WeakReference<LoanCard> lendAndDropTheCard(Shelf shelf) {
        return new WeakReference<>(shelf.lend(DICTIONARY + "#1", "M-2", DUE));
    }
```

**No local variable holds the card, on purpose.** Write `LoanCard card = shelf.lend(...)` in
`main` and that local keeps the card reachable for the rest of `main`. Block 7 would then report
`false` for reasons that have nothing to do with the shelf. A separate frame that returns only
the weak reference removes that confound.

```java
    private static boolean collected(WeakReference<?> ref) {
        for (int attempt = 0; attempt < 20 && ref.get() != null; attempt++) {
            System.gc();
```

**`System.gc()` is a request the JVM may decline, so one call proves nothing.** A `WeakReference`
is cleared by any collection that finds its referent unreachable, so the loop asks repeatedly
and stops as soon as the answer arrives. The `false` results in block 7 are the interesting
ones, and a `false` after twenty full collections means reachable rather than unlucky.

This is a teaching instrument, not a pattern to use. Production code calling `System.gc()` in a
loop is a red flag; `WeakReference` in production is for caches and for listener registries.

---

# Two experiments, run for real

## 1 · The null dereference `-Xlint:all` will not mention

```java
public class NullDesk {
    public static void main(String[] args) {
        Copy copy = null;
        System.out.println(copy.barcode());
    }
}
```

```
.toolchain\jdk-21\bin\javac.exe -Xlint:all -d . *.java
```

Zero output, exit code 0. The value is a compile-time constant `null`, the dereference is
unconditional, and `javac` still has nothing to say. There is no flag that changes this, because
nullability is not in the type system.

Then at run time, uncaught:

```
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Copy.barcode()" because the return value of "Shelf.copyWithBarcode(String)" is null
	at Desk.main(Desk.java:8)
```

**Read the message backwards.** The last clause names the expression that was null, the first
names the call that failed. Java 14 added this and Java 21 has it on by default, so the message
usually tells you which of several dots on one line was the problem. That is a habit worth
building now, because the alternative is a debugger.

The one place the compiler does reject `null`:

```
Prim.java:3: error: incompatible types: <null> cannot be converted to int
        int copiesHeld = null;
                         ^
1 error
```

**Primitives, and only primitives.** Which is the same rule seen from the other side: `int`,
`long`, `boolean`, `char` and `double` are values, and every other type you declare is a
reference with `null` in its range.

## 2 · The `<localN>` in the message, and how to get a name

Block 5 dereferences a local:

```java
Copy missing = shelf.copyWithBarcode(DESIGN_PATTERNS + "#1");
System.out.println(missing.barcode());
```

Compiled plainly:

```
Cannot invoke "Copy.barcode()" because "<local1>" is null
```

Compiled with `javac -g`:

```
Cannot invoke "Copy.barcode()" because "missing" is null
```

**The class file carries no local variable table unless you ask for it.** Both messages come
from the same source line. If a `NullPointerException` names a `<localN>` you cannot place,
recompile that file with `-g` and run it again rather than reading bytecode. Build tools usually
pass `-g` already, so this is mostly a hand-compilation surprise.

---

## Which design to write

The corpus reference solution returns `null` from no method, holds no mutable object in two
places, and has no defensive copy to get right. It reaches that position by making the shared
things immutable. `Title` and `Loan` are records, `Member` is final with no setters, and a copy
travels as a `String copyId` rather than as an object.

Write `Copy` as it is here when the thing genuinely has changing state that several parts of the
design need to see, and then decide who may change it. Write it as the corpus does when a
snapshot is enough. The second is the answer more often than a C++ background expects, because
the reason C++ code passes objects around by reference is a cost Java has already paid.
