# NOTES — twelve files, one closed set, and the two switches that check it

## Run it first

From the directory holding the sources:

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

Real output, verbatim:

```
--- 1. the closed set, and what the compiler knows about it
  Node.class.isSealed()          : true
  permitted implementations      : FileNode, DirectoryNode, SymlinkNode
  kindOf(/logs)                  : DIRECTORY
  kindOf(/logs/app.log)          : FILE
  kindOf(/logs/latest)           : FILE  (the shortcut was followed)
  kindOf on a raw shortcut       : java.lang.IllegalStateException: resolve() must never hand back an unresolved shortcut: /logs/app.log
--- 2. what the record header generated
  Listing.class.isRecord()       : true
  components                     : String path, EntryKind kind, int childCount
  toString                       : Listing[path=/logs, kind=DIRECTORY, childCount=3]
  accessor one.childCount()      : 3
  one == two                     : false
  one.equals(two)                : true
  hashCodes agree                : true
  List.of(one).contains(two)     : true
  FileNode.class.isRecord()      : false  (its content changes after construction)
--- 3. the compact constructor, doing the two jobs it exists for
  "/logs/" normalised to         : /logs
  equal to the version without   : true
  a negative child count         : java.lang.IllegalArgumentException: childCount must not be negative: -1
  a relative path                : java.lang.IllegalArgumentException: path must be absolute: logs
  new SymlinkNode("/logs/")      : SymlinkNode[target=/logs]
  equal to the version without   : true
  a null target                  : java.lang.NullPointerException: target
--- 4. one switch, five answers, no casts
  describe(/logs)                : a directory of 3 entries
  describe(/var)                 : an empty directory
  describe(/logs/app.log)        : a file of 36 characters
  describe(/logs/empty.log)      : an empty file
  describe(/logs/latest)         : a shortcut to /logs/app.log
  describe(null)                 : java.lang.NullPointerException
--- 5. var, where the right-hand side already names the type
  var rows = new ArrayList<Listing>() : ArrayList, holding 1
  var row  = new Listing(..)           : Listing
  Optional<Node> found = fs.find(..)   : Optional is all 'var found' would have told a reader
--- 6. Optional, as a return type and nothing else
  find(/logs/latest)             : Optional[SymlinkNode[target=/logs/app.log]]
  find(/var/missing.log)         : Optional.empty
  map then orElse                : nothing is there
  get() on the empty one         : java.util.NoSuchElementException: No value present
  orElseThrow, which get(path) is: NoSuchPathException: /var/missing.log
  an Optional reference is itself nullable:
    java.lang.NullPointerException: Cannot invoke "java.util.Optional.isPresent()" because "Main.cached" is null
--- 7. a text block holding the expected listing
  actual.equals(expected)        : true
  the expected block, as stored, three lines and one trailing newline:
    /logs/app.log   FILE      0
    /logs/empty.log FILE      0
    /logs/latest    FILE      0
--- 8. a shortcut that points at itself
  read(/tmp/loop)                : java.lang.IllegalStateException: shortcut loop reached from /tmp/loop
  find(/tmp/loop) does not follow: Optional[SymlinkNode[target=/tmp/loop]]
```

Four lines in there are the lesson. `permitted implementations` in block 1, the pair
`one == two : false` and `one.equals(two) : true` in block 2, and `describe(/logs/empty.log) : an
empty file` in block 4, which is a guard rather than a variant.

`javac -Xlint:all` prints three warnings and no errors, all three the same one:

```
NoSuchPathException.java:5: warning: [serial] serializable class NoSuchPathException has no definition of serialVersionUID
final class NoSuchPathException extends RuntimeException {
      ^
```

Every `RuntimeException` subclass in the corpus draws that warning too, and none of them declares
the field. It is not part of this lesson, and it is here rather than silently suppressed.

`out/` holds twelve class files from twelve sources, with no `$1` or `$2` among them. The two
lambdas in `Main` and the one in `MiniFileSystem.get` produce no files, for the reason J5's notes
give: they are spun up at run time from an `invokedynamic` call site.

---

## `Node.java` — one line, and the only line that closes the set

```java
sealed interface Node permits FileNode, DirectoryNode, SymlinkNode {
}
```

**Both words carry a guarantee, and they are different guarantees.**

`sealed` promises that the implementations are known at compile time. What that buys is the
completeness check on every `switch` over a `Node`, anywhere in the program. Drop `sealed` and both
switches in `Nodes.java` stop compiling until each grows a `default`, and a `default` is what makes
a missing case silent.

`permits` is the list the check is measured against. It is also refused when it is absent, because
the implementations are in other files:

```
Node.java:1: error: sealed class must have subclasses
sealed interface Node {
       ^
```

What breaks without the seal, concretely: a class in any other file can implement `Node`. Then the
set is open, `Nodes.kindOf` needs a fallback, and the fallback answers for a node nobody has
thought about yet. Block 1 prints `Node.class.isSealed() : true` and the three names, which is the
promise as the JVM sees it.

The cost, stated plainly. Every new variant is an edit to this file, so a team that wants to add
node kinds without touching your code cannot. That is the trade, and it is the same one
`std::variant` makes.

---

## `FileNode.java` and `DirectoryNode.java` — the two that are not records

```java
final class FileNode implements Node {
    private String content;
    …
    void write(String content) {
        this.content = content;
    }
}
```

**`write` is the whole reason this is a class.** A record's components are final, so the record form
would make every write build a new `FileNode` and rebind it in its parent directory. Block 2 prints
`FileNode.class.isRecord() : false` for that reason and no other.

`DirectoryNode` is a class for a second reason worth separating. Its state is a `TreeMap`, and a
record's generated accessor returns the component itself, with no copy. A
`record DirectoryNode(TreeMap<String, Node> children)` would hand the live map to every caller.
Then `addChild`'s invariant, no two children sharing a name, would be enforceable by nobody.

`final` on both classes is required, not stylistic. A permitted subclass of a sealed type has to be
`final`, `sealed` or explicitly `non-sealed`:

```
T2.java:2: error: sealed, non-sealed or final modifiers expected
class Circle implements Shape {}
^
```

That message is the one people find puzzling. The sealed type's promise is about the whole
hierarchy, so an open leaf would reopen the set through the back door.

---

## `SymlinkNode.java` — the record, and the constructor that earns it

```java
record SymlinkNode(String target) implements Node {
```

**One line replaces eight**, and the eight are what
`corpus/file-system/curveballs/01-shortcuts/reference-patch/SymlinkNode.java` actually contains: a
field, a constructor, an accessor and their braces. The record also arrives with `equals`,
`hashCode` and `toString` that the class version never had, and block 3 prints the `toString` as
`SymlinkNode[target=/logs]`.

```java
    SymlinkNode {
        Objects.requireNonNull(target, "target");
```

**The invariant: no `SymlinkNode` exists anywhere with a null target.** So no reader of `target()`
checks for null, and `follow` in `MiniFileSystem` can call `get(link.target())` without a guard.
Drop this line and the null is discovered at the first resolution, several calls away from the
constructor that allowed it.

```java
        if (!target.startsWith("/")) {
            throw new IllegalArgumentException("a shortcut target must be absolute: " + target);
        }
```

**A relative target can never resolve**, because `MiniFileSystem` only walks from the root. Refusing
it here turns a permanent lookup failure into a construction failure with the bad value in the
message. Without it, the symptom is `NoSuchPathException` for a path that reads as though it should
exist.

```java
        if (target.length() > 1 && target.endsWith("/")) {
            target = target.substring(0, target.length() - 1);
        }
```

**This assignment is legal only here, and only because of when the compact constructor runs.** The
generated constructor assigns the parameters to the fields after this body finishes, so `target` is
an ordinary local until then. Try the same assignment in any other method and:

```
T6.java:2: error: cannot assign a value to final variable childCount
```

What it guarantees: `new SymlinkNode("/logs/")` and `new SymlinkNode("/logs")` are `equals` and
share a `hashCode`, which block 3 prints as two `true` lines. What breaks without it: two records
naming one path are unequal, land in different `HashMap` buckets, and `describe` reports a shortcut
to `/logs/` where the rest of the program says `/logs`.

---

## `Listing.java` — the record as a returned value

```java
record Listing(String path, EntryKind kind, int childCount) {
```

**Three components, and `equals` compares all three.** This is the row `ls` hands back, and its
whole job is to be compared, printed and put in lists. Block 2 is the J6 pair on one object:
`one == two` is `false` and `one.equals(two)` is `true`.

`List.of(one).contains(two) : true` is the same fact from the side that bites in production.
`contains` calls `equals`, so a hand-written value class with no `equals` answers `false` for a row
that is in the list. That is J6's failure, and a record cannot produce it.

```java
    Listing {
        Objects.requireNonNull(path, "path");
        …
        if (childCount < 0) {
```

**Every constructor call passes through here, because a record has exactly one canonical
constructor.** That is what makes it the right place for the check, and it is J4's rule about
validating before the object exists.

The negative-count check is not defensive noise. `childCount` is read by `isEmptyDirectory`, and a
negative value would make that method answer `false` for a directory with impossible state, rather
than throwing where the impossible value was introduced.

```java
    boolean isEmptyDirectory() {
        return kind == EntryKind.DIRECTORY && childCount == 0;
    }
```

**A record can carry behaviour. What it cannot carry is a field outside the header.** Try:

```
T7.java:2: error: field declaration must be static
    private int hits;
                ^
  (consider replacing field with record component)
```

The parenthesised hint is `javac` naming the only legal place for state in a record.

---

## `Nodes.java` — the two switches, and the `default` that is absent

```java
    static EntryKind kindOf(Node node) {
        return switch (node) {
            case FileNode file -> EntryKind.FILE;
            case DirectoryNode dir -> EntryKind.DIRECTORY;
            case SymlinkNode link -> throw new IllegalStateException(…);
        };
    }
```

**The absence of a `default` is the load-bearing part of this method.** Three cases over three
permitted types is complete, so the compiler accepts it with no fallback. Add `SymlinkNode` to
`permits` without adding the third case and you get exactly one error, at this line:

```
Nodes.java:14: error: the switch expression does not cover all possible input values
        return switch (node) {
               ^
1 error
```

That line number is from the real corpus file, not this one. Write it with a `default` instead and
the same edit compiles clean and answers `DIRECTORY` for a shortcut, which `from-cpp.md` measures.

**The third arm throws for a case that cannot happen**, and that is deliberate rather than lazy.
`MiniFileSystem.resolve` follows shortcuts, so an unresolved one arriving here means a caller went
around it. The compiler demanded a decision, and the decision is written down where the next reader
finds it. Block 1 prints it firing when called directly.

```java
    static String describe(Node node) {
        return switch (node) {
            case FileNode file when file.content().isEmpty() -> "an empty file";
            case FileNode file -> "a file of " + file.content().length() + " characters";
```

**The guarded case must be first, and the unguarded one must exist.** Those are two separate rules
and each has its own error.

Reverse the pair and the unguarded case is unreachable:

```
T9.java:5: error: this case label is dominated by a preceding case label
```

Delete the unguarded case and the switch is incomplete, because a guard is a condition and the
compiler will not decide whether it always holds:

```
TA.java:3: error: the switch expression does not cover all possible input values
```

What the guard guarantees: an empty file reports as `an empty file` rather than `a file of 0
characters`. Block 4 prints both arms firing.

```java
            case SymlinkNode(String target) -> "a shortcut to " + target;
```

**A record deconstruction pattern.** It matches the type and binds the component in one step, so
there is no `link.target()` call and no variable holding a node nobody uses. It only works because
`SymlinkNode` is a record, and it reads the normalised target, because that is the only value the
record ever holds.

**No cast appears anywhere in either switch.** That is the difference from an `instanceof` chain,
and it is the part that shortens a skeleton sprint. Each case binds a typed variable whose scope is
that arm.

---

## `MiniFileSystem.java` — where the closed set is used

```java
    Optional<Node> find(String path) {
        return Optional.ofNullable(bound(path));
    }
```

**`ofNullable`, never `of`.** `Optional.of(null)` throws `NullPointerException`, which would turn
"nothing is there" into a crash inside the method whose job is to report it. Block 6 prints the
empty answer as `Optional.empty`.

The corpus's own `PathResolver.find` returns a bare `Node` and documents "or null if nothing is
there." This is that method with the sentence moved into the return type. What it guarantees: a
caller cannot forget, because there is no way to use an `Optional<Node>` as a `Node`.

```java
    private Node bound(String path) {
        …
            return null;
```

**Private, and it is the only place in the class where a null exists.** That containment is the
point of the pair. One method deals in null because a `TreeMap` lookup does; nothing outside this
class ever sees one.

```java
    Node get(String path) {
        return find(path).orElseThrow(() -> new NoSuchPathException(path));
    }
```

**`orElseThrow` with a supplier, not `get()`.** The supplier runs only when there is nothing there,
and the exception it builds names the path. Block 6 prints the two side by side:

```
  get() on the empty one         : java.util.NoSuchElementException: No value present
  orElseThrow, which get(path) is: NoSuchPathException: /var/missing.log
```

An unchecked `get()` is the null dereference `Optional` was introduced to remove, moved one call
later. The message it produces has no path in it, and no caller of a file system can act on it.

```java
        switch (parent.child(leaf)) {
            case null -> parent.addChild(leaf, new FileNode(content));
```

**`case null` is required here, and its absence would be a null-pointer exception on the common
path.** A pattern switch throws `NullPointerException` for a null selector unless a null case is
written. And `parent.child(leaf)` returns null for a name that does not exist yet, which is what
creating a file means.

These four arms are curveball 01's fix to `write`. `PATCH.md` charges 4 of the file's 8 lines to it,
and the reason is the fourth arm: a shortcut sitting at this name is replaced rather than followed.

```java
    private Node follow(Node node, String path, int hops) {
        if (!(node instanceof SymlinkNode link)) {
            return node;
        }
        if (hops >= MAX_SHORTCUT_HOPS) {
```

**The hop count is what makes `resolve` terminate**, and it is the one real algorithm change
curveball 01 asks for. `TreeResolver.java` is 16 of that curveball's 28 lines for this reason alone.

The invariant `resolve` maintains: the node it returns is never a `SymlinkNode`. Every caller
depends on it, which is why `read` and `kindOf` can throw in their shortcut arms. Block 8 shows the
guard firing on a shortcut that points at itself, and shows `find` deliberately not following.

`if (!(node instanceof SymlinkNode link))` is the `instanceof` pattern rather than the switch, and
it is the right shape for one test out of two. The switch earns its keep at three or more.

---

## `Main.java` — two blocks that are only about syntax

```java
        var rows = new ArrayList<Listing>();
        Optional<Node> found = fs.find("/logs/latest");
```

**The threshold, in two lines of the same method.** The first says `ArrayList<Listing>` on the right,
so saying it on the left twice is noise. The second says `fs.find`, which tells a reader nothing, so
the type is written out. Block 5 prints what a reader of the `var` version would have had:
`Optional` with no element type.

```java
        String expected = """
                /logs/app.log   FILE      0
```

**The closing delimiter's line sets the margin, and its position decides the trailing newline.**
Here `"""` is on its own line, so the string ends with `\n`, and the `Collectors.joining` call that
builds `actual` supplies one to match. Block 7 prints `actual.equals(expected) : true`.

What this guarantees over a concatenated string: the expected value in the source looks like the
expected value in the output. So a mismatch is visible by reading, rather than by counting `\n`
escapes. That is the only claim worth making for text blocks.

---

## What an interviewer is measuring

Not whether you can spell `sealed`. Two things.

**Whether you said the set was closed, and why.** The design sentence is this one:

> A node is a file or a directory. The requirement has no third kind, so the interface is sealed,
> and the one switch over it cannot compile with a case missing.

It also sets up the follow-up you want, which is what happens when a third kind arrives.

**Whether the answer to that follow-up is a number.** It is: 28 lines for
`file-system/curveballs/01-shortcuts`, of which 4 belong to the sealed hierarchy and 24 are the
requirement. Seven of the eight operations changed by nothing at all, because they were written in
terms of `PathResolver` rather than a map they walked themselves.

The habit that costs you the mark is reaching for `sealed` on every interface. Four of the twenty
corpus problems have a sealed hierarchy. `Appender` in `corpus/logger` is an ordinary interface
because a curveball adds a destination, and sealing it would have broken that curveball.
