---
title: 'The Standard Library and Streams'
source: 'https://academia.sh/en/courses/java-standard-library'
language: en
updated: '2026-08-17T04:41:23+00:00'
license: 'CC BY-SA 4.0'
lessons: 15
outcomes:
    - 'Uses the collections framework by choosing the appropriate data structure'
    - 'Writes declarative data transformations with the stream API'
    - 'Uses the input/output and date–time APIs correctly'
---

# The Standard Library and Streams

Using the standard library means knowing what was promised: the course measures every behavior with a single question — does the interface give this guarantee, does only this implementation do it, or does it depend on a rule the caller follows? Collections, the stream API, input/output, and date–time all pass through the same observation frame, and every use that does not rest on a guarantee is counted at the point where it breaks.

## Collections

The collections framework's interface hierarchy and implementations, the guarantees list, set, and queue choice rests on, where hash-based and sorted maps diverge, the iterator's response to a change during iteration, and the scope difference between natural order and a comparator.

### Collections Framework

Source: https://academia.sh/en/courses/java-standard-library/collections-framework

The same three implementations are run side by side for four separate families, and seven of fifteen observations come out as interface guarantees, eight as the chosen class's implementation behavior alone. The bounding measurement bounds this very distinction: on a four-element source, PriorityQueue's iteration order matches its exit order by coincidence, and once a fifth element is added the distinction becomes visible again.

Throughout the Object-Oriented Java course, one decision was always given by the language.
The visibility level decided which member would be seen, `final` decided which variable
could never be rebound, a checked exception decided which failure would stand in the
signature. The closing lesson measured that none of the three contracts — precondition,
postcondition, invariant — eliminates a flaw, they only move where it sits; but the decision
was still inside the language, and the compiler could read it from the source every time.

This lesson starts by calling a written library, and the decision is now outside the
language. `List`, `Set`, `Queue`, `Map` are each an interface, and an interface is a promise;
but who makes that promise is not read from a single place. An observed behavior can belong
to one of three sources — the interface itself, the chosen class, or a rule the caller
follows — and the three cannot be told apart in any way by the call's syntax. `ArrayList` and
`LinkedList` both accept the same `add(0)` call, both answer the same `get(0)` call; which
behavior stays the same across both, which changes when the class changes, cannot be seen
from the source text. This lesson's and this course's question is: who gives the guarantee
for an observation you are holding.

#### Three Implementations, One Question

The question's answer cannot be found with a single implementation — someone watching one
class alone confuses its behavior with the interface's promise. The measurement therefore
lines up **three** implementations that speak the same interface and asks all three the same
question: if the answer is the same across all three, the observation comes from the
interface; if the three diverge, the observation belongs to the implementation.

- **CO1** — The `Guarantee` class below is not specific to any family. `ask` only compares
  the answers it gathers; being a list, set, queue, or map does not change the criterion.
- **CO2** — If an implementation throws an exception during the call, its answer is counted
  as `"error"` and compared in the same column as the other two answers; the exception does
  not open a separate branch.

```java
// Guarantee.java — who gives a behavior's guarantee: the interface, or the implementation
import java.util.*;
import java.util.function.*;

public class Guarantee {
    static int byInterface = 0;
    static int byImplementation = 0;

    final List<String> names = new ArrayList<>();
    final List<Supplier<Object>> suppliers = new ArrayList<>();

    Guarantee add(String name, Supplier<Object> supplier) {
        names.add(name);
        suppliers.add(supplier);
        return this;
    }

    void header(String family) {
        StringBuilder sb = new StringBuilder(String.format("%-36s", family));
        for (String name : names) sb.append(String.format("%-15s", name));
        sb.append("guarantee giver");
        System.out.println(sb);
    }

    void ask(String question, Function<Object, String> observe) {
        List<String> answers = new ArrayList<>();
        for (Supplier<Object> u : suppliers) {
            String y;
            try {
                y = observe.apply(u.get());
            } catch (RuntimeException e) {
                y = "error";
            }
            answers.add(y);
        }
        boolean shared = new HashSet<>(answers).size() == 1;
        if (shared) byInterface++; else byImplementation++;
        StringBuilder sb = new StringBuilder(String.format("%-36s", question));
        for (String y : answers) sb.append(String.format("%-15s", y));
        sb.append(shared ? "interface" : "implementation");
        System.out.println(sb);
    }

    static void summary() {
        System.out.printf("observations: %d interface guarantees, %d implementation behaviors%n",
                byInterface, byImplementation);
    }
}
```

`add` adds one implementation to a family; every family calls it three times. `ask` puts one
question to all three implementations at once and runs a single counter: if the answer set
drops to a single element (`new HashSet<>(answers).size() == 1`), the question is written into
the interface column; if it does not drop, into the implementation column. The rule is
mechanical and has no subject — it does not know which family is being asked, it only counts
answers.

#### Four Families, Fifteen Observations

- **CO3** — The source list `SOURCE` is five fixed elements and stays unchanged across all
  four families; it is written into the map in the same order too.
- **CO4** — All fifteen counted rows come from a single run of the same program; `summary`
  gives that run's total, counting no other row.

```java
// Framework.java — separates the four families' observable promises
import java.util.*;

public class Framework {
    static final List<String> SOURCE = List.of("zulu", "delta", "alfa", "carli", "bravo");

    static Map<String, Integer> fill(Map<String, Integer> m) {
        for (int i = 0; i < SOURCE.size(); i++) m.put(SOURCE.get(i), i);
        return m;
    }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Guarantee list = new Guarantee()
                .add("ArrayList", () -> new ArrayList<>(SOURCE))
                .add("LinkedList", () -> new LinkedList<>(SOURCE))
                .add("List.of", () -> List.copyOf(SOURCE));
        list.header("list");
        list.ask("is insertion order preserved",
                o -> String.valueOf(new ArrayList<>((List<String>) o).equals(SOURCE)));
        list.ask("is indexed access available", o -> ((List<String>) o).get(1));
        list.ask("does equality look at content",
                o -> String.valueOf(o.equals(new ArrayList<>(SOURCE))));
        list.ask("can an element be added", o -> {
            try { ((List<String>) o).add("extra"); return "yes"; }
            catch (RuntimeException e) { return "no"; }
        });
        list.ask("can a null value be held", o -> {
            try { ((List<String>) o).add(null); return "yes"; }
            catch (RuntimeException e) { return "no"; }
        });
        System.out.println();

        Guarantee set = new Guarantee()
                .add("HashSet", () -> new HashSet<>(SOURCE))
                .add("LinkedHashSet", () -> new LinkedHashSet<>(SOURCE))
                .add("TreeSet", () -> new TreeSet<>(SOURCE));
        set.header("set");
        set.ask("is a duplicate element accepted", o -> {
            Set<String> s = (Set<String>) o;
            s.add("alfa");
            return String.valueOf(s.size());
        });
        set.ask("is iteration order insertion order",
                o -> String.valueOf(new ArrayList<>((Set<String>) o).equals(SOURCE)));
        set.ask("is a null value accepted", o -> {
            try { ((Set<String>) o).add(null); return "yes"; }
            catch (RuntimeException e) { return "no"; }
        });
        System.out.println();

        Guarantee queue = new Guarantee()
                .add("ArrayDeque", () -> new ArrayDeque<>(SOURCE))
                .add("LinkedList", () -> new LinkedList<>(SOURCE))
                .add("PriorityQueue", () -> new PriorityQueue<>(SOURCE));
        queue.header("queue");
        queue.ask("element coming out first", o -> ((Queue<String>) o).poll());
        queue.ask("what is returned when empty", o -> {
            Queue<String> q = (Queue<String>) o;
            while (q.poll() != null) { }
            return String.valueOf(q.poll());
        });
        queue.ask("is iteration order exit order", o -> {
            Queue<String> q = (Queue<String>) o;
            List<String> iteration = new ArrayList<>(q);
            List<String> exit = new ArrayList<>();
            String s;
            while ((s = q.poll()) != null) exit.add(s);
            return String.valueOf(iteration.equals(exit));
        });
        System.out.println();

        Guarantee map = new Guarantee()
                .add("HashMap", () -> fill(new HashMap<>()))
                .add("LinkedHashMap", () -> fill(new LinkedHashMap<>()))
                .add("TreeMap", () -> fill(new TreeMap<>()));
        map.header("map");
        map.ask("is the key unique", o -> {
            Map<String, Integer> m = (Map<String, Integer>) o;
            m.put("alfa", 99);
            return String.valueOf(m.size());
        });
        map.ask("is the key view live", o -> {
            Map<String, Integer> m = (Map<String, Integer>) o;
            Set<String> g = m.keySet();
            m.put("extra", 5);
            return String.valueOf(g.contains("extra"));
        });
        map.ask("is iteration order insertion order",
                o -> String.valueOf(new ArrayList<>(((Map<String, Integer>) o).keySet())
                        .equals(SOURCE)));
        map.ask("is a null key accepted", o -> {
            try { ((Map<String, Integer>) o).put(null, 0); return "yes"; }
            catch (RuntimeException e) { return "no"; }
        });
        System.out.println();

        Guarantee.summary();
    }
}
```

```
list                                ArrayList      LinkedList     List.of        guarantee giver
is insertion order preserved        true           true           true           interface
is indexed access available         delta          delta          delta          interface
does equality look at content       true           true           true           interface
can an element be added             yes            yes            no             implementation
can a null value be held            yes            yes            no             implementation

set                                 HashSet        LinkedHashSet  TreeSet        guarantee giver
is a duplicate element accepted     5              5              5              interface
is iteration order insertion order  false          true           false          implementation
is a null value accepted            yes            yes            no             implementation

queue                               ArrayDeque     LinkedList     PriorityQueue  guarantee giver
element coming out first            zulu           zulu           alfa           implementation
what is returned when empty         null           null           null           interface
is iteration order exit order       true           true           false          implementation

map                                 HashMap        LinkedHashMap  TreeMap        guarantee giver
is the key unique                   5              5              5              interface
is the key view live                true           true           true           interface
is iteration order insertion order  false          true           false          implementation
is a null key accepted              yes            yes            no             implementation

observations: 7 interface guarantees, 8 implementation behaviors
```

**Seven** of the fifteen rows gathered in the interface column, **eight** in the
implementation column. The split is not arbitrary, and a line can be drawn cutting the table
in two: **what is held** stands in the interface, **how and in what order it is held** is
left to the implementation. That a list preserves insertion order, gives back what it holds
by index, and that its equality looks at content — all three are the same across the three
classes. That adding the same element a second time will not grow a set, that pulling from an
empty queue gives `null` — these are the same too. By contrast, which order a set or map is
walked in, which element comes out of the front of a queue, and whether a container accepts a
null value diverge row by row. **Most** of a behavior list drawn by watching a single class —
just `HashMap`, just `PriorityQueue` — is not the promise the interface gives.

#### Optional Operation: The Interface's Own Reservation

Two rows in the table deserve a separate reading: "can an element be added" and "can a null
value be held" were both marked `implementation`, because the container `List.of` produces
rejects both, `ArrayList` and `LinkedList` accept both. But the `Collection` interface's own
definition calls the `add` method an **optional operation**: the interface defines the
method, and at the same time says an implementation may reject it. This splits the guarantee
question in two — whether the call **will succeed** is one question, what happens **when it
fails** is a separate question. The first's answer, in the table above, depended on the
implementation; the second is measured below.

- **CO5** — All four container kinds share the same trial: try adding a single element, if
  rejected write the exception's class name, if accepted write `"succeeded"`.

```java
// Rejection.java — do all rejecting implementations throw the same exception
import java.util.*;

public class Rejection {
    static String attempt(Runnable action) {
        try { action.run(); return "succeeded"; }
        catch (RuntimeException e) { return e.getClass().getSimpleName(); }
    }

    public static void main(String[] args) {
        List<String> mutableList = new ArrayList<>(List.of("a"));
        List<String> fixedList = List.of("a");
        Set<String> fixedSet = Set.of("a");
        Map<String, Integer> fixedMap = Map.of("a", 1);

        System.out.println("ArrayList.add     : " + attempt(() -> mutableList.add("b")));
        System.out.println("List.of(...).add  : " + attempt(() -> fixedList.add("b")));
        System.out.println("Set.of(...).add   : " + attempt(() -> fixedSet.add("b")));
        System.out.println("Map.of(...).put   : " + attempt(() -> fixedMap.put("b", 2)));
    }
}
```

```
ArrayList.add     : succeeded
List.of(...).add  : UnsupportedOperationException
Set.of(...).add   : UnsupportedOperationException
Map.of(...).put   : UnsupportedOperationException
```

Three separate container kinds, three separate interfaces, give the same rejection with the same
class. If this row entered the table above, it would be marked `interface` because of the shared
answer — yet the rejection **itself** never happens at all for `ArrayList`, it only happens for
the three fixed containers. The contradiction stays only apparent, because two separate
questions are getting mixed up. The answer to "is the addition accepted" depends on the
implementation, and was counted that way in the previous table. The answer to "which exception
falls if it is not accepted" is bound to a single class named in the `Collection` interface's
javadoc, and does not change across any of the three interfaces. The interface does not
guarantee a behavior here, it guarantees **the form of a rejection**: every implementation that
rejects, rejects with the same name, the same unchecked exception class. This is why the phrase
"optional operation" means not "the operation does not exist" but "the operation may exist, or
fall with its name."

#### The Bounding Measurement: Agreement Is Weak Evidence

The framework's rule is a simple inference: if the three answers are the same, it is the
interface. This is an **inference**, it does not look at documentation — and the inference
itself can be wrong. The queue table's third row (`is iteration order exit order`) gave
`false` for `PriorityQueue`; for `ArrayDeque` and `LinkedList`, `true`. This row's divergence
with the five-element source was already seen. What happens if the same question is asked of
a smaller, four-element source?

- **CO6** — The four-element source is five-element `SOURCE` with its first element (`zulu`)
  removed; once the fifth element is added back, the source returns to being byte-for-byte
  `SOURCE` again.

```java
// Coincidence.java — observed agreement is not proof of a guarantee
import java.util.*;

public class Coincidence {
    static final List<String> SOURCE = List.of("zulu", "delta", "alfa", "carli", "bravo");

    static boolean iterationEqualsExit(List<String> source) {
        PriorityQueue<String> pq = new PriorityQueue<>(source);
        List<String> iteration = new ArrayList<>(pq);
        List<String> exit = new ArrayList<>();
        String s;
        while ((s = pq.poll()) != null) exit.add(s);
        return iteration.equals(exit);
    }

    public static void main(String[] args) {
        List<String> fourElements = SOURCE.subList(1, 5);
        System.out.println("four-element source            : " + fourElements);
        System.out.println("is iteration order exit order  : " + iterationEqualsExit(fourElements));

        List<String> fiveElements = new ArrayList<>(fourElements);
        fiveElements.add(0, "zulu");
        System.out.println("source once fifth element added: " + fiveElements);
        System.out.println("is iteration order exit order  : " + iterationEqualsExit(fiveElements));
    }
}
```

```
four-element source            : [delta, alfa, carli, bravo]
is iteration order exit order  : true
source once fifth element added: [zulu, delta, alfa, carli, bravo]
is iteration order exit order  : false
```

With the four-element source, `PriorityQueue`'s iteration order matches its exit order
**exactly** — by the inference's own rule, this would count as an interface guarantee. But
once the fifth element is added and the source becomes `SOURCE` itself again, the same
question gives `false`: the result already seen in the queue table. The only difference is
the elements themselves; the queue interface never promised anything about iteration order,
neither at four elements nor at five. The match that appeared in the four-element case was
not a promise the library gave, it was those five names coincidentally lining up with the
heap's layout.

This is how to read the criterion built through the lesson in reverse: **agreement observed
weakly supports a guarantee, divergence observed proves the absence of a guarantee for
certain.** Three implementations giving the same answer could be an interface's promise, or it
could equally well be a coincidence born from the data set's smallness; the only way to tell
them apart is to change the data set and ask the same question again. Divergence's evidentiary
power is higher than this, because no coincidence can force three separate classes into three
separate answers — a diverging row always shows a real implementation difference.

#### Summary

- Who gives a behavior's guarantee cannot be seen from the call's syntax; three
  implementations have to be run side by side and their answers compared. If the answer is
  the same across all three, the observation comes from the interface; if it diverges, it
  comes from the chosen class.
- Of fifteen questions asked across four families (list, set, queue, map), **seven** came out
  as interface guarantees, **eight** as implementation behavior. What is held stands in the
  interface; how and in what order it is held is left to the implementation.
- The `Collection` interface defines methods like `add` as optional operations: whether they
  are accepted depends on the implementation, but which exception falls when rejected stays
  the same across three separate interfaces — not the rejection itself, but the rejection's
  **form**, is the interface's promise.
- Bounding measurement: on a four-element source, `PriorityQueue`'s iteration order matches
  its exit order by coincidence; once a fifth element is added, the distinction becomes
  visible again. Observed agreement is not proof of a guarantee; observed divergence proves
  the absence of one for certain.
- The `Gauge` core (the class-file information reader) is not used in this lesson; the
  measurement rests only on run-time observation, because the question asked concerns the
  promise the library gives, not the compiler.

#### Next Step

This lesson measured four families as a whole and counted how many of each family's promises
come from the interface. But the distinction between "list," "set," and "queue" itself was
never questioned — all three are collections, all three can accept the same workload. The
next lesson gives the same data to all three interfaces at once and measures **which promise
each one gives and which information each one silently drops**; it is there that
`LinkedList` being both a list and a queue is seen to give the same call two separate
meanings.

### List, Set, and Queue

Source: https://academia.sh/en/courses/java-standard-library/list-set-and-queue

The same six-element workload is given to a list, a set, and a queue implementation at once, and what each one drops from the input is counted: the list drops nothing, the set drops three pieces of information, the queue drops only positional access. The bounding measurement arises from LinkedList being both a list and a queue at the same time: both interfaces give a remove promise, the promises collide in a single call line, and the identically written remove(1) does two separate jobs — both guarantees hold, the gap is in the line the caller writes, and the break is silent.

The previous lesson measured four families as a single whole and counted how many of fifteen
observations came from the interface, how many from the chosen class. But the choice between
list, set, and queue itself was never questioned — all three are collections, all three
accept the same `add` call. This lesson opens that choice: when the same data is given to all
three at once, what does each one hold, what does it silently let go of? The answer will show
that none is "better" than the other; all three are designed to answer a different question.

The Data Structures course established list, stack, queue, and set as abstract data types and
compared implementation options by complexity; that comparison is not repeated here. The
question here is not size, it is promise: the difference between choosing a list and choosing
a set is, before the speed of operations, a commitment about which information will be
**kept**.

Even the three interfaces' names carry a promise: a list evokes an ordered index, a set the
mathematical set's property of uniqueness, a queue a physical line's two ends. This lesson
drops the intuition the names evoke down to a measurable number: facing the same input, how
many pieces of information does each interface keep, how many does it sacrifice — and whether
what is sacrificed is coincidence or the interface's own promise is tested separately.

#### The Same Workload, Three Promises

- **CO7** — The workload is a fixed six-element list carrying two duplicate values (`apple`
  and `pear` each appear twice); all three families are filled from the same list, in the
  same order.
- **CO8** — Three pieces of information are measured, independent of one another: whether the
  duplicate element is held, whether insertion order is preserved, whether a position (index)
  is directly accessible. How much information a family drops is the sum of these three.

Only one implementation was chosen from each family (`ArrayList`, `HashSet`, `ArrayDeque`) —
comparing three implementations to separate interface from implementation, as in the previous
lesson, is not repeated in this lesson, because the question here is different: while the
implementation stays fixed, how much do the three interfaces diverge from one another?
`HashSet` was chosen deliberately, because its iteration order does not coincide with the
input; had an order-preserving set implementation been chosen, this section's measurement
would be hidden.

```java
// ThreePromises.java — gives the same workload to three interfaces, counts what each drops
import java.util.*;

public class ThreePromises {
    static final List<String> WORKLOAD = List.of("apple", "pear", "apple", "cherry", "pear", "plum");

    static void measure(String family, Collection<String> container) {
        boolean duplicatesHeld = container.size() == WORKLOAD.size();
        boolean orderHeld = new ArrayList<>(container).equals(WORKLOAD);
        boolean indexAccess = container instanceof List;
        int dropped = (duplicatesHeld ? 0 : 1) + (orderHeld ? 0 : 1) + (indexAccess ? 0 : 1);
        System.out.printf("%-8s size=%-3d duplicates=%-6s order=%-6s index=%-6s dropped=%d%n",
                family, container.size(), duplicatesHeld, orderHeld, indexAccess, dropped);
    }

    public static void main(String[] args) {
        measure("list", new ArrayList<>(WORKLOAD));
        measure("set", new HashSet<>(WORKLOAD));
        measure("queue", new ArrayDeque<>(WORKLOAD));
    }
}
```

```
list     size=6   duplicates=true   order=true   index=true   dropped=0
set      size=4   duplicates=false  order=false  index=false  dropped=3
queue    size=6   duplicates=true   order=true   index=false  dropped=1
```

Three rows tell three separate commitments. `ArrayList` **drops nothing** from the input: all
six elements remain, order is preserved, any position can be reached directly with `get`.
`HashSet` drops **all three at once**: the six-element input shrinks to four (the two
duplicates were dropped), the remaining four's iteration order does not coincide with the
input, there is no positional access at all. `ArrayDeque` drops only **one**: it holds the
duplicate too, preserves order too, the only thing it drops is positional access.

#### The Promise Comes from the Promise, Not from Complexity

The three pieces of information the set drops are not a flaw, they are the price of
uniqueness: if a container promises it will hold every element once, the moment the second
`apple` arrives it is either rejected or merges with the first — either way produces the same
result, a size that stays small. The set's promise is single: **uniqueness**. It says nothing
about what order will result, because which internal structure secures uniqueness (a hash
table, a balanced tree, an insertion-ordered linked list) mixes into the order, and the
interface does not choose that internal structure — the previous lesson's measurement of
`HashSet`, `LinkedHashSet`, and `TreeSet` giving three separate answers to the same question
was exactly this.

Unlike uniqueness, the order information a set drops is not a loss the interface **requires**
— this can be measured directly. The same workload is given to two set implementations and
whether the result coincides with first-seen order (the order in which the four elements
left after dropping duplicates first appeared in the input) is checked.

```java
// Uniqueness.java — is the order a set drops something the interface requires
import java.util.*;

public class Uniqueness {
    static final List<String> WORKLOAD = List.of("apple", "pear", "apple", "cherry", "pear", "plum");
    static final List<String> FIRST_SEEN = List.of("apple", "pear", "cherry", "plum");

    static boolean orderMatchesFirstSeen(Collection<String> container) {
        return new ArrayList<>(container).equals(FIRST_SEEN);
    }

    public static void main(String[] args) {
        Set<String> hash = new HashSet<>(WORKLOAD);
        Set<String> linked = new LinkedHashSet<>(WORKLOAD);
        System.out.println("HashSet iteration order                 : " + hash);
        System.out.println("HashSet order same as first-seen         : " + orderMatchesFirstSeen(hash));
        System.out.println("LinkedHashSet iteration order            : " + linked);
        System.out.println("LinkedHashSet order same as first-seen   : " + orderMatchesFirstSeen(linked));
    }
}
```

```
HashSet iteration order                 : [plum, apple, cherry, pear]
HashSet order same as first-seen         : false
LinkedHashSet iteration order            : [apple, pear, cherry, plum]
LinkedHashSet order same as first-seen   : true
```

`LinkedHashSet` keeps the same uniqueness promise while also preserving order; `HashSet` does
not. So a set dropping order is not something the interface imposes, it is a choice made by
the chosen class — just like the previous lesson's "is iteration order insertion order" rows,
measured across four families, being marked `implementation`. The only thing the interface
requires is uniqueness; order, like positional access, is an add-on that can change from class
to class.

The queue's promise is single too and separate from the set's: **the end.** The `Queue`
interface says, with the trio `offer`, `poll`, `peek`, "it is added at one end, removed from
one end, and that end is fixed"; it does not say which element will wait at that end.
`ArrayDeque` preserves insertion order in this workload not by coincidence but by its own
implementation choice (an array used end to end); the previous lesson's `PriorityQueue`,
speaking the same interface, giving `false` to the same question already showed that order is
not the queue's promise. The only piece of information the queue loses here is positional
access, because a container promising an end does not give an address to a middle element —
giving an address is the list's job.

The list, by contrast, carries **two** promises at once: **order and duplication.** Every
element added to a list stays at its own position, even if its equal was added earlier before
it; every position can be addressed with `get`. These two promises are not the sum of what the
set and the queue give separately — the list does not promise uniqueness (both `apple`s
remain), and it is not limited to a single end like the queue (it can be read from any
position). The three interfaces correspond to three separate needs, and the choice is made by
looking at which information **needs to be kept.** Carrying these two promises at once has a
cost too, but that cost is not measured in this lesson: inserting at a middle position or
searching for whether a value exists carries a cost that varies by implementation, and that
cost was already counted in the Data Structures course. What is measured here is only which
information **stays or does not stay**; how long it stays is a separate axis.

The previous lesson's optional-operation observation appears once more here. A fixed set
produced with `Set.of` and a fixed list produced with `List.of` both rejected an add attempt
with the same exception; this lesson does not measure the rejection's **form**, it measures
which information stays when it is not rejected. The two measurements complete each other: one
answers "what happens if the addition is accepted," the other "what do we see if the addition
is rejected." Both come to the same conclusion: understanding a container's behavior takes
more than a single call's syntax — which interface gives which promise has to be known
separately.

#### The Bounding Measurement: The Same Object, Two Meanings

`LinkedList` is the framework's only dual-identity class: it implements both `List` and
`Queue` (through `Deque`) at the same time. In the measurement above, the `queue` row was
represented by `ArrayDeque`, the `list` row by `ArrayList`; both were pure classes carrying a
single interface's promise. `LinkedList` is not pure — the same object, depending on which
reference it is called through, can carry both the list's and the queue's promise, and when
these two promises share the same method name, which one applies cannot be seen from the
call's syntax. The rule measured in the Object-Oriented Java course applies here exactly: what
method a call binds to is decided not by the runtime type, but by the **declared type**.

- **CO9** — A single `LinkedList` object is built and assigned to two variables: one declared
  with type `List`, the other with type `Queue`. Both point to the same object; this could be
  confirmed with `==`, but the measurement does not need it, because the second call's effect
  changes the state the first one left behind.

```java
// Ambiguity.java — same object, two interfaces, two separate remove methods
import java.util.*;

public class Ambiguity {
    public static void main(String[] args) {
        LinkedList<Integer> actual = new LinkedList<>(List.of(10, 20, 30, 40));
        List<Integer> listRef = actual;
        Queue<Integer> queueRef = actual;

        System.out.println("start                          : " + actual);
        boolean wasRemoved = queueRef.remove(1);
        System.out.println("remove(1) via queue reference  -> returned=" + wasRemoved
                + "  result=" + actual);

        Integer removedValue = listRef.remove(1);
        System.out.println("remove(1) via list reference   -> returned=" + removedValue
                + "  result=" + actual);
    }
}
```

```
start                          : [10, 20, 30, 40]
remove(1) via queue reference  -> returned=false  result=[10, 20, 30, 40]
remove(1) via list reference   -> returned=20  result=[10, 30, 40]
```

The line written is identical in both: `remove(1)`. The result is not the same at all. The
`Queue` interface has no method called `remove(int)`; the compiler boxes the argument `1` into
an `Integer` and binds it to `remove(Object)` coming from `Collection` — it searches the
container for an element whose value is `1`, does not find one, returns `false`, and
**changes nothing.** In the `List` interface, `remove(int)` is defined directly and the
compiler reads `1` as an index; the `20` sitting at the second position is deleted and the
deleted value is returned. The same two characters — `(1)` — mean "find the value and delete
it" in one place, "delete the second position" in the other.

**No guarantee is broken here.** Both interfaces keep their promise fully: `Queue` promised to
delete an element by its value, `List` promised to delete an element by its position, and both
did what they promised. The implementation is not at fault either; the object is the same
`LinkedList` object from start to finish and nothing inside it is corrupted. The gap is **in
the line the caller writes**: where two promises fall onto the same name and the same syntax,
it is the caller's obligation to say which one is wanted.

When this obligation is broken, the result is the **silent** one among the three kinds this
course measures. A call written with the intent "delete the second element" throws no
exception, **does nothing at all**, and returns `false`; if the return value is not read, the
flaw passes unnoticed. `LinkedList` speaking two interfaces at once is not a convenience here,
it is a trap — because a single class is one of the rare places where two promises can
collide within the same call's syntax.

- **CO10** — What the caller has to say is not only which interface it is speaking through,
  but in what form it writes the argument. Through the same `List`, if the argument is
  explicitly boxed as `Integer`, the "delete by value" promise is selected — the measurement
  shows this with a single reference, without opening a second variable.

```java
// Resolution.java — what decides is not the reference's type, it is the argument's type
import java.util.*;

public class Resolution {
    public static void main(String[] args) {
        LinkedList<Integer> actual = new LinkedList<>(List.of(10, 20, 30, 40));
        List<Integer> listRef = actual;

        boolean wasRemoved = listRef.remove(Integer.valueOf(1));
        System.out.println("list reference, remove(Integer.valueOf(1)) -> returned="
                + wasRemoved + "  result=" + actual);

        Integer removedValue = listRef.remove(1);
        System.out.println("list reference, remove(1) (int)            -> returned="
                + removedValue + "  result=" + actual);
    }
}
```

```
list reference, remove(Integer.valueOf(1)) -> returned=false  result=[10, 20, 30, 40]
list reference, remove(1) (int)            -> returned=20  result=[10, 30, 40]
```

The variable is the same `listRef`, the interface spoken through is the same `List<Integer>`.
Only the argument's form changed: in the first call, `Integer.valueOf(1)` is an object and
falls onto the "delete by value" promise — the container is searched for an element equal to
the value `1`, none is found, `false` is returned. In the second call, `1` is an `int`
constant and falls onto the "delete by position" promise. So the obligation does not end with
choosing the interface: the caller, when writing the argument, is also saying which promise it
wants, and can say this without realizing it. The Object-Oriented Java course measured which
side this choice is made on; what is measured here is not the side of the choice, it is that
the library leaving the choice to the caller **never asks about it anywhere.**

The reason this trap can only be shown on `LinkedList` is that it speaks two interfaces at
once. `ArrayList` speaks only `List`, `ArrayDeque` speaks only `Queue`; on those classes the
form `remove(1)` has a single counterpart and the collision never arises at all. `LinkedList`
gathers both promises in the same object; this is why the measurement can be done not on two
separate classes, but on **a single object**, with just two variables. "Delete by position"
exists only on the `List` side, not at all on the `Queue` side — the distinction arises not
from the object's behavior, but from the set of promises the two interfaces give. This is a
general rule that holds for every class in the library that implements more than one interface
at once; `LinkedList` is the closest example to showing it with a single call.

#### Summary

- When the same six-element input is given to a list, a set, and a queue, the list drops no
  information, the set drops all three at once (duplication, order, positional access), the
  queue drops only positional access.
- The three interfaces' selection criterion is not operation speed, it is promise: a list
  promises **order and duplication**, a set promises **uniqueness**, a queue promises **the
  end**. The complexity comparison was made in the Data Structures course and is not repeated
  here; what is measured here is only which information is still readable once it leaves the
  input.
- The information a set drops is not a flaw; it is the required price of keeping the
  uniqueness promise. That a queue preserves order, though, is not the interface's promise, it
  is the chosen implementation's preference.
- `LinkedList` carries both the `List` and `Queue` interfaces at once, and the two interfaces'
  `remove` promises collide within a single call's syntax: the identically written `remove(1)`
  deletes by position in one place, by value in another.
- No guarantee is broken in the collision — both interfaces keep their promise, the object
  does not change. What breaks is **the caller's obligation**, and the break is **silent**: no
  exception falls, `false` is returned, and if the return value is not read, the flaw goes
  unnoticed.

#### Next Step

In this lesson, the list, the set, and the queue each reached the element they held through a
single path: position, membership, or the end. What these three access paths had in common was
that the container was self-sufficient — finding where an element was required no information
from outside the container. A key-value mapping gives a promise different from all three: an
element is reached by its key, and the interface promises to keep that key's counterpart
unique. But half of the promise about what the key itself can be does not sit in the library,
it sits in the class the caller writes — the container only asks for the key, it does not
check whether the key behaves consistently with itself. The next lesson measures this other
half: when a key class is written incorrectly, where does the lookup land, which exception
falls, and if none falls, what stands in its place?

### Maps

Source: https://academia.sh/en/courses/java-standard-library/maps

A key-value map's three promises — the key's uniqueness, the key view's liveness, a missing key giving null — come out the same across all three implementations and belong to the interface. Iteration order, null-key acceptance, and the requirement that a key be comparable, though, diverge between the hash-based and the sorted implementation. The caller's rule is put at the center for the first time: a lookup with a key that writes equals but not hashCode returns null with no exception at all, while the same key is found silently in a sorted map, because the promise used there is compareTo.

The previous lesson measured that a list, a set, and a queue each reach an element by one of
three separate paths: position, membership, the end. All three were accesses where the
container was self-sufficient — finding the element required no information from outside the
container. A map breaks this. In a map, an element is reached by its key, and the key can be a
class the caller writes, one the library has never seen. The container only asks for the key;
it does not check whether the key behaves consistently with itself. This lesson measures where
that inconsistency lands: which promise comes from the library, which promise rests on the
shoulders of the class the caller wrote, and how the program reports it when a crack opens
between the two — or does not report it at all.

Every flaw the previous two lessons measured was instantly visible in a compiled program, or
not visible at all — if an exception was thrown, it was thrown; if not, the behavior was
consistent. A key flaw in a map opens a third path: the program compiles, runs, throws no
exception, and still works incorrectly. This third path is not a gap in the library, it is an
obligation the library leaves to the caller going unanswered.

#### The Interface Promise of Holding Key-Value Pairs

A map, like the previous two lessons, is tested with three implementations (`HashMap`,
`LinkedHashMap`, `TreeMap`); the measurement engine is the same, only the questions asked in
this lesson are new.

- **CO11** — The engine is **semantically identical** to the previous two lessons; only the
  questions asked in this lesson are added. Adding a new question to the `Guarantee` class
  does not change the existing questions' behavior.
- **CO12** — The map is filled with five keys, in the same order, with the same `fill` helper.

```java
// Guarantee.java — who gives a behavior's guarantee: the interface, or the implementation
import java.util.*;
import java.util.function.*;

public class Guarantee {
    static int byInterface = 0;
    static int byImplementation = 0;

    final List<String> names = new ArrayList<>();
    final List<Supplier<Object>> suppliers = new ArrayList<>();

    Guarantee add(String name, Supplier<Object> supplier) {
        names.add(name);
        suppliers.add(supplier);
        return this;
    }

    void header(String family) {
        StringBuilder sb = new StringBuilder(String.format("%-40s", family));
        for (String name : names) sb.append(String.format("%-15s", name));
        sb.append("guarantee giver");
        System.out.println(sb);
    }

    void ask(String question, Function<Object, String> observe) {
        List<String> answers = new ArrayList<>();
        for (Supplier<Object> u : suppliers) {
            String y;
            try {
                y = observe.apply(u.get());
            } catch (RuntimeException e) {
                y = "error";
            }
            answers.add(y);
        }
        boolean shared = new HashSet<>(answers).size() == 1;
        if (shared) byInterface++; else byImplementation++;
        StringBuilder sb = new StringBuilder(String.format("%-40s", question));
        for (String y : answers) sb.append(String.format("%-15s", y));
        sb.append(shared ? "interface" : "implementation");
        System.out.println(sb);
    }

    static void summary() {
        System.out.printf("observations: %d interface guarantees, %d implementation behaviors%n",
                byInterface, byImplementation);
    }
}
```

```java
// Maps.java — the interface promise of holding key-value pairs, and the hash/sorted split
import java.util.*;

public class Maps {
    static final List<String> SOURCE = List.of("zulu", "delta", "alfa", "carli", "bravo");

    static Map<String, Integer> fill(Map<String, Integer> m) {
        for (int i = 0; i < SOURCE.size(); i++) m.put(SOURCE.get(i), i);
        return m;
    }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Guarantee map = new Guarantee()
                .add("HashMap", () -> fill(new HashMap<>()))
                .add("LinkedHashMap", () -> fill(new LinkedHashMap<>()))
                .add("TreeMap", () -> fill(new TreeMap<>()));
        map.header("map");
        map.ask("is the key unique", o -> {
            Map<String, Integer> m = (Map<String, Integer>) o;
            m.put("alfa", 99);
            return String.valueOf(m.size());
        });
        map.ask("is the key view live", o -> {
            Map<String, Integer> m = (Map<String, Integer>) o;
            Set<String> g = m.keySet();
            m.put("extra", 5);
            return String.valueOf(g.contains("extra"));
        });
        map.ask("what is the answer for a missing key",
                o -> String.valueOf(((Map<String, Integer>) o).get("missing")));
        map.ask("is iteration order insertion order",
                o -> String.valueOf(new ArrayList<>(((Map<String, Integer>) o).keySet())
                        .equals(SOURCE)));
        map.ask("is a null key accepted", o -> {
            try { ((Map<String, Integer>) o).put(null, 0); return "yes"; }
            catch (RuntimeException e) { return "no"; }
        });
        System.out.println();
        Guarantee.summary();
    }
}
```

```
map                                     HashMap        LinkedHashMap  TreeMap        guarantee giver
is the key unique                       5              5              5              interface
is the key view live                    true           true           true           interface
what is the answer for a missing key    null           null           null           interface
is iteration order insertion order      false          true           false          implementation
is a null key accepted                  yes            yes            no             implementation

observations: 3 interface guarantees, 2 implementation behaviors
```

Three rows gather in the interface column. Writing to a key a second time does not grow the
size — all three stay at five keys, because the `Map` interface directly defines that a key
will be unique. The set `keySet()` returns is a **view bound to the container itself**: the
`"extra"` key added to the map afterward also shows up in the view taken earlier; the view is
not a point-in-time copy, it is a window opened onto the container itself. The third row is a
map's most often forgotten promise: looking up a key that does not exist throws no exception,
it returns null. All three separate implementations keep these three promises exactly; when
choosing a map, these three can be accepted without argument.

This third promise has its own limit, and a single line is enough to see it. If the map also
accepts null as a **value**, `get` returning null cannot distinguish between two separate
situations: the key does not exist at all, or the key exists and its counterpart is already
null.

```java
// NullOrMissing.java — get(key) cannot tell absence apart from a null value
import java.util.*;

public class NullOrMissing {
    public static void main(String[] args) {
        Map<String, Integer> m = new HashMap<>();
        m.put("alfa", null);
        System.out.println("get(alfa)            : " + m.get("alfa"));
        System.out.println("get(missing)         : " + m.get("missing"));
        System.out.println("containsKey(alfa)    : " + m.containsKey("alfa"));
        System.out.println("containsKey(missing) : " + m.containsKey("missing"));
    }
}
```

```
get(alfa)            : null
get(missing)         : null
containsKey(alfa)    : true
containsKey(missing) : false
```

Both `get` calls give the same answer, null; but one describes a key that really does not
exist, the other a key that exists and is deliberately bound to null. This is where `get`
alone is not enough: to fully read the absence promise, `containsKey` is needed as a separate
call. The interface's promise is "a missing key returns null"; it is not "if null is returned,
the key is missing" — the two are not the same proposition.

#### Where Hash-Based and Sorted Diverge

The table's bottom two rows separate `HashMap`/`LinkedHashMap` from `TreeMap`. Iteration order
looks at the keys' hash value in `HashMap`, at insertion order in `LinkedHashMap`, at the
keys' natural order in `TreeMap` — all three a decision of the chosen class, not the
interface. Null-key acceptance splits along the same line: `HashMap` and `LinkedHashMap` hold
the null key in a single special slot, `TreeMap` rejects it, because placing a key requires
**comparing** it against the existing keys, and null has no order to compare.

This last observation points to a third distinction: what `TreeMap` wants from a key is
different from what `HashMap` wants. `HashMap` recognizes a key only through `equals` and
`hashCode`; `TreeMap` expects an order. This is a difference that does not show up in the
interface itself — both implement `Map<K, V>`, neither writes any extra constraint on `K` in
its signature. The difference only surfaces at run time, when a key is placed for the first
time. What happens if the key class is not sortable?

- **CO13** — The same key class writes `equals` and `hashCode` fully but declares no ordering
  at all (does not implement `Comparable`). A single `put` call, with the same single key, is
  made to all three maps.

```java
// Requirement.java — in which implementation is a sortable key required
import java.util.*;

public class Requirement {
    static class IncomparableKey {
        final String name;
        IncomparableKey(String name) { this.name = name; }
        @Override public boolean equals(Object o) {
            return o instanceof IncomparableKey a && a.name.equals(name);
        }
        @Override public int hashCode() { return name.hashCode(); }
    }

    static String attempt(Runnable action) {
        try { action.run(); return "succeeded"; }
        catch (RuntimeException e) { return e.getClass().getSimpleName(); }
    }

    public static void main(String[] args) {
        Map<IncomparableKey, Integer> hash = new HashMap<>();
        Map<IncomparableKey, Integer> linked = new LinkedHashMap<>();
        Map<IncomparableKey, Integer> sorted = new TreeMap<>();

        System.out.println("HashMap.put       : " + attempt(() -> hash.put(new IncomparableKey("a"), 1)));
        System.out.println("LinkedHashMap.put : " + attempt(() -> linked.put(new IncomparableKey("a"), 1)));
        System.out.println("TreeMap.put       : " + attempt(() -> sorted.put(new IncomparableKey("a"), 1)));
    }
}
```

```
HashMap.put       : succeeded
LinkedHashMap.put : succeeded
TreeMap.put       : ClassCastException
```

`equals` and `hashCode` are both fully written, nothing is missing — and the third row still
collapses. `HashMap` and `LinkedHashMap` only need to know the key's hash value and equality
to place it in its slot; `TreeMap`, though, has to **compare** the key against something to
place it at its position in the tree even on the first `put` call, and it has neither a
`Comparable` nor a `Comparator` in hand. The flaw is not visible at compile time — the
`Map<K, V>` definition puts no ordering constraint on `K` at all — and at run time, on the
first write, it falls by its name.

#### The Caller's Rule: Writing equals Without hashCode

The two sections above separated the interface's and the implementation's promises. A third
source remains: the rule the caller's own written key class has to follow. The `Object` class
defines a contract — if two objects are considered equal by `equals`, their `hashCode`s must
be equal too — but the compiler checks this contract nowhere. What happens when `equals` is
written and `hashCode` is not?

- **CO14** — The key class writes `equals` correctly (compares the name) and **never
  overrides** `hashCode`; the inherited `Object.hashCode()` looks at run-time identity. Two
  separate `new` calls carrying the same name produce two separate hash values.

```java
// MismatchedKey.java — a lookup with a key that has equals but no hashCode
import java.util.*;

public class MismatchedKey {
    static class Key {
        final String name;
        Key(String name) { this.name = name; }
        @Override public boolean equals(Object o) {
            return o instanceof Key a && a.name.equals(name);
        }
        // hashCode deliberately not written
    }

    public static void main(String[] args) {
        Map<Key, Integer> store = new HashMap<>();
        store.put(new Key("alfa"), 9);
        Integer found = store.get(new Key("alfa"));
        String result = String.valueOf(found);
        System.out.printf("%-30s %-9s found=%-6s expected=9%n", "key without hashCode",
                result.equals("9") ? "succeeded" : "silent", result);
    }
}
```

```
key without hashCode           silent    found=null   expected=9
```

Both the writer and the searcher use the same name, `"alfa"`; `equals` would consider them
equal. But `HashMap` consults `hashCode` first to decide which slot to look in, and the two
separate `new Key("alfa")` objects, because of the default `hashCode` inherited from `Object`,
almost certainly fall into two separate slots. The lookup never visits the slot the value sits
in and does not even get the chance to call `equals`. The result is **silent**: no exception
falls, `get` just returns null — as if the key was never added at all. Where the flaw shows up
may not be the write line, it may be a lookup line written months later.

The three-way classification this course sorts results into — `exception`, `silent`,
`ineffective` — is met here for the first time with a concrete example. The missing `hashCode`
does not fall into the `exception` class: no line crashes, the program runs from start to
finish. Had the same map been filled with thousands of keys, the flaw would still be invisible;
only a wrong-seeming number of "lost" elements would appear, and finding this loss's cause
would be a bug that has to be tracked down days later, without ever touching the source.

#### The Bounding Measurement: The Same Key Is Found in a Sorted Map

The previous measurement seems to lead to the conclusion "this key class is broken." The
bounding measurement corrects this: the same class, the same missing `hashCode`, the only
difference is the implementation chosen.

- **CO15** — The key class is **byte-for-byte identical** to the one above; `hashCode` is
  still not written. The only thing that changes is that the map is a `TreeMap`, built with a
  `Comparator`.

```java
// FoundInSorted.java — the same flawed key is found in a map relying on ordering
import java.util.*;

public class FoundInSorted {
    static class Key {
        final String name;
        Key(String name) { this.name = name; }
        @Override public boolean equals(Object o) {
            return o instanceof Key a && a.name.equals(name);
        }
        // hashCode deliberately not written
    }

    public static void main(String[] args) {
        Map<Key, Integer> sorted = new TreeMap<>(Comparator.comparing(a -> a.name));
        sorted.put(new Key("alfa"), 9);
        Integer found = sorted.get(new Key("alfa"));
        System.out.println("TreeMap.get, same flawed key      : " + found);
    }
}
```

```
TreeMap.get, same flawed key      : 9
```

The same class, the same missing method, and this time the lookup **finds** it. The
difference is not in the key, it is in the implementation reading the key: `TreeMap` never
looks at `hashCode` to find a position, it only calls the given `Comparator`, and that
`Comparator` only reads the `name` field. This prevents the first measurement from being
misread. The key class itself was not the flawed thing; the flaw was a **mismatch** between
the promise a class gives (only `equals`) and the promise the chosen implementation expects
(`hashCode` and `equals` together). The same key, in the hands of an implementation with
different expectations, works flawlessly.

Placing these two measurements side by side brings the course's question to exactly this
point. Saying "this class is broken" when describing a flaw is, most of the time, the wrong
question; the right question is "which promise does this class give, and which promise does
the side reading it expect?" `HashMap` expects `hashCode` and the key does not give it — the
flaw is there. `TreeMap` expects only an order, and the key, indirectly through a
`Comparator`, gives it — the flaw is not there. The key class never changed; what changed was
which contract the key was tested against.

#### Summary

- A map's three promises — the key's uniqueness, the key view's liveness, a missing key
  returning null — came out the same across all three implementations and belonged to the
  interface.
- Iteration order and null-key acceptance diverged between the hash-based and the sorted
  implementation; at the root of the divergence is `TreeMap`'s requirement to **compare** the
  key.
- A key that cannot be sorted works fine in `HashMap` but falls with `ClassCastException` on
  the first write in `TreeMap` — the flaw shows up not at compile time, but at run time.
- The caller's rule: when `equals` is written but `hashCode` is not, a lookup returns null
  with no exception at all; the flaw does not surface on the write line, it surfaces on the
  lookup line.
- Bounding measurement: the same key class with the missing method is found without trouble in
  a sorted map, because the promise sought there is `compareTo`/`Comparator`. The flaw was not
  in the key, it was in the key's promise not matching the implementation's expectation.

#### Next Step

This lesson measured whether a map can find its key; what happens, though, if the container
**changes while being walked**? Adding a new element to the same container, or removing one,
while advancing over a map or a set with `for` is a situation all three families run into.
The next lesson measures this situation: some implementations throw an exception immediately
and by name, some complete a wrong walk without saying anything at all — and which one
happens is decided, again, not by the interface but by the implementation. The "the flaw
compiles, runs, and silently gives the wrong result" pattern seen in this lesson will meet us
there once more, for a different reason.

### Iterators and Concurrent Modification

Source: https://academia.sh/en/courses/java-standard-library/iterators-and-concurrent-modification

The iterator's basic promise — NoSuchElementException once exhausted — comes out byte-for-byte identical across three separate implementations. But if the container changes during iteration, three separate answers appear: one that fails fast, one that walks over a snapshot, one that says nothing at all. Fail-fast itself turns out to be a best effort, not a guarantee: the same removal throws an exception at one position while leaving a loop that finishes silently, one short, at the second-to-last element. The bounding measurement shows the iterator's own remove falls outside this rule.

The previous lesson measured whether a map could find its key, and split the flaw's source
into three: interface, implementation, caller's rule. This lesson tests the same trio not on
the container's **resting** state but **while it is being iterated.** Adding a new element to
the same container, or removing one, while walking a list, a set, or a map with `for` is a
situation all three families share. The library does not give this a single answer.

Iteration always assumes an order: the iterator infers where the next element is from the
internal state the previous call left behind. If the container changes in the middle of
iteration, a mismatch opens between this internal state and the container's real state. The
problem itself is the same across every implementation; what gets measured is how each one
handles this mismatch — silently ignoring it, stopping by name, or being designed so it never
arises at all.

#### The Iterator's Interface Promise

Before entering the subject of modification, the iterator's side that does not break is
measured. The `Iterator` interface defines the `hasNext`/`next` pair and also defines what
`next` will do on an exhausted iterator.

- **CO16** — All three containers are filled with the same three elements and walked to
  exhaustion; then one more `next` call is made. The only thing measured is that last call's
  result.

```java
// BasicPromise.java — do all three implementations behave the same once next is exhausted
import java.util.*;

public class BasicPromise {
    static String attempt(Iterable<String> container) {
        Iterator<String> it = container.iterator();
        while (it.hasNext()) it.next();
        try {
            it.next();
            return "next succeeded (not expected)";
        } catch (NoSuchElementException e) {
            return e.getClass().getSimpleName();
        }
    }

    public static void main(String[] args) {
        List<String> source = List.of("a", "b", "c");
        System.out.println("ArrayList  : " + attempt(new ArrayList<>(source)));
        System.out.println("HashSet    : " + attempt(new HashSet<>(source)));
        System.out.println("ArrayDeque : " + attempt(new ArrayDeque<>(source)));
    }
}
```

```
ArrayList  : NoSuchElementException
HashSet    : NoSuchElementException
ArrayDeque : NoSuchElementException
```

All three families give the same exception, with the same name. This is a promise the
`Iterator` interface directly defines: the `next` contract requires throwing this exception
on an exhausted iterator. As long as the container itself does not change — as long as it is
only being iterated — the three implementations cannot be told apart. The distinction begins
once the container **itself** changes during iteration.

This measurement is the same shape as the previous lessons' "answer for a missing key"
measurement: when the container **never changes**, only being queried, three implementations
gather around a single behavior. The distinction always shows up when the container's state
**changes** — writing a key in the previous lesson, the container changing while being
iterated in this one.

#### Three Answers: Fail-Fast, Snapshot, Silence

- **CO17** — All four containers are filled with the same four elements, and while being
  walked with a for-each, the moment the second element is processed, a **fifth element** is
  added to the container. What is measured is how this addition affects the iteration: an
  exception, silent continuation, and if it continues, how many elements are seen. The change
  is made at **the same step** in all four containers; only the container changes.

```java
// ThreeAnswers.java — three separate answers if the container changes during iteration
import java.util.*;
import java.util.concurrent.*;

public class ThreeAnswers {
    static String attempt(Iterable<String> container, Runnable change) {
        int counted = 0;
        try {
            for (String s : container) {
                counted++;
                if (counted == 2) change.run();
            }
            return "no exception, counted=" + counted;
        } catch (ConcurrentModificationException e) {
            return e.getClass().getSimpleName() + ", counted=" + counted;
        }
    }

    public static void main(String[] args) {
        List<String> failingList = new ArrayList<>(List.of("a", "b", "c", "d"));
        System.out.println("ArrayList (fail-fast)                     : "
                + attempt(failingList, () -> failingList.add("extra")));

        Set<String> failingSet = new HashSet<>(List.of("a", "b", "c", "d"));
        System.out.println("HashSet (fail-fast)                       : "
                + attempt(failingSet, () -> failingSet.add("extra")));

        List<String> snapshotBased = new CopyOnWriteArrayList<>(List.of("a", "b", "c", "d"));
        System.out.println("CopyOnWriteArrayList (snapshot)           : "
                + attempt(snapshotBased, () -> snapshotBased.add("extra")));

        Map<String, Integer> staysSilent = new ConcurrentHashMap<>();
        staysSilent.put("a", 1); staysSilent.put("b", 2);
        staysSilent.put("c", 3); staysSilent.put("d", 4);
        System.out.println("ConcurrentHashMap (says nothing)          : "
                + attempt(staysSilent.keySet(), () -> staysSilent.put("extra", 5)));
    }
}
```

```
ArrayList (fail-fast)                     : ConcurrentModificationException, counted=2
HashSet (fail-fast)                       : ConcurrentModificationException, counted=2
CopyOnWriteArrayList (snapshot)           : no exception, counted=4
ConcurrentHashMap (says nothing)          : no exception, counted=5
```

Four rows show three separate promises, and none of the three is written in the `Iterator`
interface itself — `Iterator` never defines what will happen to a modification during
iteration, it leaves the definition to the chosen class. `ArrayList`'s and `HashSet`'s
iterators check, on every `next` call, whether the container has changed structurally, and if
it has, they stop immediately, by name: because the addition happened after the second
element, the third `next` call falls in both. That two separate data structures (an
array-based list, a hash-based set) keep the same counter is not a coincidence — `AbstractList`
and `HashMap` (which `HashSet` internally wraps) share the same `modCount` mechanism;
fail-fast is not a single class's trait, it is part of the framework's design pattern.

`CopyOnWriteArrayList`, by contrast, has already taken a copy of the array the moment
iteration starts; whatever gets added to the container, that copy has four elements and the
iterator sees all four, without noticing anything. The fourth row is different from both
previous ones: `ConcurrentHashMap` neither throws an exception nor freezes on a fixed copy —
in this run, the fifth element added also enters the iteration, and a total of five elements
are counted, but this is not a promise this API gives, it is only this run's result; the
library only guarantees it **will not throw** an exception, it does not guarantee which
elements it will see.

#### Fail-Fast Is a Best Effort, Not a Guarantee

`ArrayList`'s "fail-fast" behavior shows its own limit in the next measurement. The iterator
does not catch every modification; it only keeps a counter, and only checks that counter when
`next` is called. If the removed element is the list's **second-to-last**, the loop already
finishes without calling `next` again, and the check never runs at all.

- **CO18** — The same five-element list is walked twice; once the middle element (index 2) is
  removed when iteration reaches it, once the second-to-last element (index 3) is. The only
  thing that changes is the removed position.

```java
// SilentEnding.java — fail-fast does not always work
import java.util.*;

public class SilentEnding {
    static void attempt(String label, int indexToRemove) {
        List<String> list = new ArrayList<>(List.of("a", "b", "c", "d", "e"));
        List<String> visited = new ArrayList<>();
        try {
            for (String s : list) {
                visited.add(s);
                if (visited.size() - 1 == indexToRemove) {
                    list.remove(indexToRemove);
                }
            }
            System.out.println(label + " -> no exception, visited=" + visited);
        } catch (ConcurrentModificationException e) {
            System.out.println(label + " -> " + e.getClass().getSimpleName()
                    + ", visited=" + visited);
        }
    }

    public static void main(String[] args) {
        attempt("if index 2 (middle) is removed", 2);
        attempt("if index 3 (second-to-last) is removed", 3);
    }
}
```

```
if index 2 (middle) is removed -> ConcurrentModificationException, visited=[a, b, c]
if index 3 (second-to-last) is removed -> no exception, visited=[a, b, c, d]
```

Same class, same for-each, same single removal — the result says two separate things. When
the middle element is removed, what is expected happens: the next `next` call catches the
counter mismatch and the exception falls. When the second-to-last element is removed, though,
the list's size drops from five to four, and the loop's internal cursor, instead of moving to
the next element, now equals the list's new size; `hasNext` reads this as "iteration is done"
and the loop ends **without calling `next` at all** — the last element (`"e"`) is never seen.
No exception at all, but the iteration is incomplete too: four elements were seen, the fifth
was silently skipped.

This is the misleading side of the name "fail-fast." `ArrayList`'s iterator is not an observer
that catches every modification; it is a **best-effort** check that only does a counter
comparison when `next` is called. Java's own documentation defines this behavior not as a
guarantee but as a tool that helps catch bugs early — and this measurement shows a situation
where even that tool itself can be skipped.

The practical consequence of this is that a program's correctness cannot rest on the sentence
"no exception was thrown." Both runs worked with the same class, the same loop, a single
removal operation; one reported the flaw by name, the other lost an element without saying
anything. Whether the check runs depends on where the removed position sits in the list — and
since this is often information that depends on run-time data, the same source code can look
flawless on some inputs and produce an incomplete iteration on others.

Where the check is done makes this best effort's boundary even clearer. `hasNext` never
performs a consistency check at all; only `next` and a few methods like it check.

- **CO19** — After one element is read, an element is added to the container; right after
  that, `hasNext` is called first, then `next`. What is measured is whether both throw an
  exception at the same time, or only one does.

```java
// HasNextCheck.java — the check only runs inside next()
import java.util.*;

public class HasNextCheck {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(List.of("a", "b", "c"));
        Iterator<String> it = list.iterator();
        it.next();
        list.add("extra");
        System.out.println("hasNext() after the change : " + it.hasNext());
        try {
            it.next();
            System.out.println("next() succeeded (not expected)");
        } catch (ConcurrentModificationException e) {
            System.out.println("next()    -> " + e.getClass().getSimpleName());
        }
    }
}
```

```
hasNext() after the change : true
next()    -> ConcurrentModificationException
```

`hasNext` silently returns `true` even after the change; the check only kicks in when `next`
is called. This makes concrete where the best effort stops: the counter comparison does not
run on every container operation, only at the moments the iterator **advances.** `hasNext`
saying "yes, there is more" is not a guarantee; there is no guarantee at all that the next
step will succeed.

#### The Bounding Measurement: The Rule Forbids Changing from Outside the Container

The measurements so far have all been the rule "do not change the container while iterating"
being broken. But what exactly does this rule mean? The iterator's **own** `remove` method
changes the container too — so why does it not throw the same exception?

- **CO20** — The same five-element list is walked with an open `Iterator`, and two elements
  are removed not directly through the list's `remove`, but through the iterator's own
  `remove`.

```java
// IteratorRemove.java — the iterator's own remove does not break the rule
import java.util.*;

public class IteratorRemove {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(List.of("a", "b", "c", "d", "e"));
        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            String s = it.next();
            if (s.equals("b") || s.equals("d")) {
                it.remove();
            }
        }
        System.out.println("result with the iterator's own remove : " + list);
    }
}
```

```
result with the iterator's own remove : [a, c, e]
```

No exception falls at all, and the removal completes without a hitch: both elements are
permanently gone. This does not contradict the previous two measurements; it requires reading
the rule more accurately. The rule is not "do not change the collection" — it is "do not
change the collection **from outside the iterator.**" When `Iterator.remove()` is called, the
iterator updates the container's internal counter (`modCount`) itself, and at the same time
advances its own expected value too; at the next `next` call the two are still equal, and the
check passes silently. The list's own `remove`, by contrast, does not know about this second
counter and only changes the first; the difference surfaces at the next `next` call. The
fail-fast mechanism does not ask "did the container change," it asks "did the container change
**through a path the iterator does not know about.**"

This distinction also explains why the rule exists. An iterator tracks which element of the
container it is sitting on through an internal position (an index in array-based containers, a
node reference in linked structures). When the container changes from outside, this internal
position can become invalid — if an element has been removed, the next `next` call can land
either on the wrong element or on a position that no longer exists at all. Because
`Iterator.remove()` updates its own record itself, this danger never arises at all; a `remove`
coming from outside, though, shifts the position without the iterator's knowledge and leaves
it inconsistent. Fail-fast is a design decision that prefers stopping by name over silently
landing on the wrong element in the face of this inconsistency.

This confirms, once again, the three lessons' shared conclusion. The rule does not forbid a
behavior — if it set a blunt prohibition like "do not change the collection," then
`Iterator.remove()` itself would always throw an exception too, yet it does not. The rule
separates **who is informed**: a change coming through the iterator's own path is an informed
change, a change coming through any other path of the container is not. Two calls that
produce the same result (an element permanently removed) are, from the iterator's point of
view, two entirely separate events.

All three answers are internally consistent, but none gives the guarantee the other gives.
Fail-fast says "stop rather than work incorrectly"; snapshot says "whatever the iteration
sees, it is shown a consistent view"; silent continuation says "I will never stop, but I make
no promise about the list of what I saw." All three are legitimate promises, and all three sit
outside the `Iterator` interface, as the chosen class's own decision — the interface only
guarantees the `hasNext`/`next` pair, it never speaks about what will happen to the container
changing during iteration.

#### Summary

- The iterator's basic promise — `NoSuchElementException` once `next` is exhausted — came out
  byte-for-byte identical across all three implementations; this is the `Iterator` interface's
  direct promise and does not break as long as the container never changes.
- When the container changed during iteration, three separate answers were measured across
  four implementations: `ArrayList` and `HashSet` fail fast and by name, `CopyOnWriteArrayList`
  stays on the copy taken at the moment iteration started and never sees the change,
  `ConcurrentHashMap` continues without throwing an exception but does not guarantee which
  elements it will see.
- Fail-fast is not a guarantee, it is a best effort: the same removal throws an exception on
  the middle element, but on the second-to-last element it finishes the loop one element short
  with no warning at all — the result depends on where the removed position sits in the list.
- Bounding measurement: the iterator's own `remove` does not break the same rule, because the
  rule is not "do not change," it is "do not change from outside the iterator";
  `Iterator.remove()` updates the container's internal counter and the iterator's expected
  value at the same time.
- None of the three answers is written in the `Iterator` interface; which one is chosen is
  decided only by the chosen class, and all three are internally consistent, independent
  promises.

#### Next Step

This lesson measured the container **changing** during iteration; what happens, though, when
the container never changes and only the iteration **order** is in question? In every
measurement so far, a container's own implementation decided which order its own elements
would be given in. The next lesson moves this decision outside the container: the ordering
rule becomes an object the caller writes, independent of the container, and whether that
object is consistent becomes a separate question that has to be tested before the container
itself. The pattern seen in this lesson continues there too: a rule can be written, can
compile, can give the correct result on most inputs, and still fall silently into wrong
behavior on a particular input.

### Comparison and Sorting

Source: https://academia.sh/en/courses/java-standard-library/comparison-and-sorting

Natural order sits in a type's own compareTo, the comparator in a separate object the caller gives from outside; the same data can be arranged in two separate orders. Stable sorting is List's own promise and comes out byte-for-byte identical across two separate list implementations. The caller's rule: a comparator inconsistent with equals collapses two separate elements into one in a sorted set, a non-transitive comparator silently sorts wrong on small input and falls by name on large enough input. The bounding measurement shows the same comparator loses no element in a list — what produces the flaw is not the comparator, it is the container that turns it into a uniqueness decision.

The previous four lessons measured the promises a container gives its own element. Ordering
asks a different question: who decides **which order** a container's elements will be given
in? Sometimes the decision sits inside the type itself, sometimes it is a separate object in
the caller's hand — and this lesson opens that distinction and measures where an externally
given rule's own flaws land.

The pattern the previous lesson showed continues here too: a rule can compile, can work
correctly on most inputs, and still either silently produce the wrong result or fall by name
on a particular input. Sorting's difference is that the rule now sits **outside** the
container, in an object the caller writes — and that object's own internal consistency is a
separate matter that has to be tested before the container's own behavior.

#### Natural Order Is the Type's Promise, the Comparator Is the Caller's

When a type implements `Comparable` and writes its own `compareTo`, this order is called
**natural order** and is part of the type's own definition — it sits inside the class, it is
compiled together with the class. A `Comparator` is not like this: it is a separate object the
caller writes at the call site, without ever touching the class.

- **CO21** — The same three records are sorted in two separate orders: once by the record's
  own `compareTo` (by name), once by a `Comparator` the caller gives (by length). The record
  class is the same in both.

```java
// NaturalOrder.java — natural order sits inside the type, the comparator outside
import java.util.*;

public class NaturalOrder {
    record Item(String name, int length) implements Comparable<Item> {
        @Override public int compareTo(Item o) { return name.compareTo(o.name); }
    }

    public static void main(String[] args) {
        List<Item> list = new ArrayList<>(List.of(
                new Item("zulu", 4), new Item("delta", 5), new Item("alfa", 4)));

        List<Item> natural = new ArrayList<>(list);
        Collections.sort(natural);
        System.out.println("natural order (Item.compareTo, by name)          : " + natural);

        List<Item> external = new ArrayList<>(list);
        external.sort(Comparator.comparingInt(Item::length));
        System.out.println("externally given comparator (by length)          : " + external);
    }
}
```

```
natural order (Item.compareTo, by name)          : [Item[name=alfa, length=4], Item[name=delta, length=5], Item[name=zulu, length=4]]
externally given comparator (by length)          : [Item[name=zulu, length=4], Item[name=alfa, length=4], Item[name=delta, length=5]]
```

The same three records, the same `List`, the same `sort` call family — and two separate
results. `Collections.sort(natural)` takes no ordering information at all, because `Item`
itself has already given a promise by writing `compareTo`; where this promise is written is
known and it is in a single place. `external.sort(...)` does exactly the opposite: without
`Item` changing at all, the caller defines its own rule (`length`) right at the call site. Both
sortings being valid shows that sorting has no **single correct answer** — what is valid is
being explicit about which promise is being used.

The importance of this distinction comes from `compareTo` being compiled together with the
class: every piece of code holding an `Item` class sees the same natural order, because the
promise is written once, in the class's own file. A `Comparator`, though, can be rewritten at
every call site; the same `Item` list can be sorted by name in one place, by length in
another, by a third field in a third place, and all three are equally valid. Natural order
carries the claim "this type has a single canonical order"; a comparator makes no such claim,
it only provides an order suited to that call's need.

#### Stable Sorting: The Interface's Own Guarantee

`List.sort` does not just produce an order — its documentation also states it will preserve
the **relative order** of elements considered equal. Whether this promise really comes out the
same across two separate implementations can be measured.

- **CO22** — The same seven records, each with a key and a first-order number, are written to
  `ArrayList` and `LinkedList` in the same order. The key repeats; the first-order number is
  unique and is used to read which record came first after sorting.

```java
// Stability.java — is stable sorting identical across two list implementations
import java.util.*;

public class Stability {
    record Entry(int key, int firstOrder) {}

    public static void main(String[] args) {
        List<Entry> source = new ArrayList<>();
        int[] keys = {3, 1, 3, 2, 1, 3, 2};
        for (int i = 0; i < keys.length; i++) source.add(new Entry(keys[i], i));

        List<Entry> array = new ArrayList<>(source);
        List<Entry> linked = new LinkedList<>(source);
        array.sort(Comparator.comparingInt(Entry::key));
        linked.sort(Comparator.comparingInt(Entry::key));

        System.out.println("after ArrayList  : " + array);
        System.out.println("after LinkedList : " + linked);
        System.out.println("are the two results byte-for-byte identical : " + array.equals(linked));
    }
}
```

```
after ArrayList  : [Entry[key=1, firstOrder=1], Entry[key=1, firstOrder=4], Entry[key=2, firstOrder=3], Entry[key=2, firstOrder=6], Entry[key=3, firstOrder=0], Entry[key=3, firstOrder=2], Entry[key=3, firstOrder=5]]
after LinkedList : [Entry[key=1, firstOrder=1], Entry[key=1, firstOrder=4], Entry[key=2, firstOrder=3], Entry[key=2, firstOrder=6], Entry[key=3, firstOrder=0], Entry[key=3, firstOrder=2], Entry[key=3, firstOrder=5]]
are the two results byte-for-byte identical : true
```

Both arrays are sorted by key, and within every key group `firstOrder` is **increasing**: the
two records with key `1` stand in the order `1, 4`, the three records with key `3` in the
order `0, 2, 5` — none of them changed places. `ArrayList` is array-based, `LinkedList` is
node-based; despite being two separate internal structures, the result is byte-for-byte
identical. This is not something left to the implementation, like the "iteration order"
measured in previous lessons: `List.sort` requires stability directly in its own contract, and
both implementations keep this requirement exactly. Here, stable sorting is the interface's
promise, not the chosen class's preference.

This forms a sharp contrast with what was measured in the previous lesson: questions like
iteration order and null-key acceptance were left to the implementation and diverged across
the three families. Stability is not like that — the `List` interface's `sort` method
definition binds every class that implements this method. A method's contract sometimes says
"every implementation gives the same answer," sometimes it says "the implementation
chooses"; both can sit side by side within the same interface, and which is which can be
confirmed not only by reading the documentation but also by running multiple implementations.

#### The Caller's Rule: When the Comparator Is Inconsistent with equals

A `TreeSet` or `TreeMap` decides uniqueness not by `equals`, but by whether the given
comparator returns `0`. What happens if the comparator considers two different elements
equal?

- **CO23** — Two records are **different** by `equals` (their names differ) but have the same
  length. The same two records are added to a `TreeSet` built on length and a `HashSet` that
  looks at names.

```java
// Inconsistent.java — what a sorted set does when the comparator is inconsistent with equals
import java.util.*;

public class Inconsistent {
    record Item(String name, int length) {}

    public static void main(String[] args) {
        Item a = new Item("apple", 4);
        Item c = new Item("kiwi", 4);
        System.out.println("a.equals(c) : " + a.equals(c));

        Set<Item> sortedSet = new TreeSet<>(Comparator.comparingInt(Item::length));
        sortedSet.add(a);
        sortedSet.add(c);
        System.out.println("set sorted by length, size : " + sortedSet.size());
        System.out.println("set contents               : " + sortedSet);

        Set<Item> hashSet = new HashSet<>();
        hashSet.add(a);
        hashSet.add(c);
        System.out.println("HashSet size (same two records)     : " + hashSet.size());
    }
}
```

```
a.equals(c) : false
set sorted by length, size : 1
set contents               : [Item[name=apple, length=4]]
HashSet size (same two records)     : 2
```

`equals` finds the two records **different**; `HashSet` reflects this correctly, size `2`.
`TreeSet`, though, never asks its key `equals` at all — it only calls the comparator, and
because the lengths are equal, it gets `0`. `TreeSet`'s uniqueness rule says "if
`compareTo`/`Comparator` returns `0`, it is the same element"; the second `add` call is
therefore not counted as an insertion, it is treated as an attempt to **overwrite** the first
element, and the size stays at `1`. The flaw is not visible at compile time, and no exception
falls either — `kiwi` silently disappears.

This comes from the same family as the "writing equals without writing hashCode" flaw
measured in the previous lesson, but from a separate mechanism. There, what was missing was
`hashCode`, and the result was the lookup **failing to find.** Here, no method is missing at
all; both `equals` and `compareTo`/`Comparator` are fully written, but the two are **mutually
inconsistent.** `TreeSet`'s and `TreeMap`'s own documentation explicitly demands this
consistency: the comparator is expected to be "consistent with" `equals`, but this expectation
is not a type constraint the compiler can check — it is only a sentence in a contract, and if
the caller does not read it, no warning is given at all.

#### A Non-Transitive Comparator: Silent on Small, Exception on Large

A comparator's second obligation is **transitivity**: if `a` is less than `b` and `b` is less
than `c`, then `a` must be less than `c` too. What happens if this does not hold?

- **CO24** — The same numbers are split into three groups (by `% 3` remainder) and the
  comparator compares these three groups in a cyclic order: group 0 less than 1, group 1 less
  than 2, group 2 less than 0. This cycle breaks transitivity from the start. Random numbers
  produced with the same fixed seed are sorted at two separate sizes (`50` and `100`).

```java
// FullMeasurement.java — a non-transitive comparator: silent on small input, exception on large
import java.util.*;

public class FullMeasurement {
    static int compare(int x, int y) {
        int tx = x % 3, ty = y % 3;
        if (tx == ty) return 0;
        if ((tx == 0 && ty == 1) || (tx == 1 && ty == 2) || (tx == 2 && ty == 0)) return -1;
        return 1;
    }

    static int violationCount(Integer[] array) {
        int count = 0;
        for (int i = 0; i < array.length; i++)
            for (int j = i + 1; j < array.length; j++)
                if (compare(array[i], array[j]) > 0) count++;
        return count;
    }

    static Integer[] randomArray(int n) {
        Integer[] array = new Integer[n];
        Random r = new Random(42);
        for (int i = 0; i < n; i++) array[i] = r.nextInt(1_000_000);
        return array;
    }

    public static void main(String[] args) {
        Integer[] small = randomArray(50);
        Arrays.sort(small, FullMeasurement::compare);
        System.out.println("n=50  -> no exception, violation count=" + violationCount(small));

        Integer[] large = randomArray(100);
        try {
            Arrays.sort(large, FullMeasurement::compare);
            System.out.println("n=100 -> no exception (not expected)");
        } catch (IllegalArgumentException e) {
            System.out.println("n=100 -> " + e.getClass().getSimpleName());
        }
    }
}
```

```
n=50  -> no exception, violation count=195
n=100 -> IllegalArgumentException
```

The fifty-element array sorts silently — no exception at all — but the result is wrong **even
by the comparator's own rule**: there are `195` cases where a pair of elements violates the
cyclic rule, meaning the array never settled into a consistent order at all, it only looks
like it did. In the hundred-element array, though, the algorithm's internal merge steps
**detect** that the order they assumed does not hold, and throw an exception by name:
"comparison method violates its general contract." The threshold depends on the data and sits
somewhere between `50` and `100` here; small arrays can finish without doing enough work to
trigger the sorting algorithm's internal consistency check, large arrays run into that check.
Non-transitivity extracts two separate costs: a **silent wrong result** at small scale, an
**exception** at large scale.

The difference between these two costs comes from the sorting algorithm's own internal
structure. On small inputs, the sorting algorithm processes the data by splitting and merging
less, and can finish without ever running into the comparator's cyclic contradiction; as the
input grows, an internal assumption the algorithm holds while merging the intermediate pieces
it produces itself (that the two pieces being merged are each internally consistently ordered)
breaks because of the comparator's contradiction, and the algorithm catches this with its own
check. This check is not a promise of the `Comparator` interface, it is an internal safety
measure of the chosen sorting algorithm; another algorithm could have returned with the same
data without ever throwing an exception, only with a wrong result.

#### The Bounding Measurement: Not the Comparator, the Container's Use of It

The previous two measurements might seem to show the comparator itself is flawed. What happens
if the same length comparator is given this time only for **sorting**, to a `List` and not a
`TreeSet`?

- **CO25** — The same inconsistent comparator (the length comparator from `Inconsistent.java`)
  is this time used to sort a three-record list; two of the records have the same length but
  are different by `equals`.

```java
// NotLostInList.java — the same comparator loses no element in a list
import java.util.*;

public class NotLostInList {
    record Item(String name, int length) {}

    public static void main(String[] args) {
        List<Item> list = new ArrayList<>(List.of(
                new Item("pear", 5), new Item("apple", 4), new Item("kiwi", 4)));
        list.sort(Comparator.comparingInt(Item::length));
        System.out.println("sorted list size : " + list.size());
        System.out.println("sorted list      : " + list);
    }
}
```

```
sorted list size : 3
sorted list      : [Item[name=apple, length=4], Item[name=kiwi, length=4], Item[name=pear, length=5]]
```

All three records are still there; `apple` and `kiwi`, having the same length, stand next to
each other, but both stay in the list, and because of stability `apple` (which came first in
the input) is written before `kiwi`. In the `TreeSet` measurement, `kiwi` disappeared; here it
does not. The difference is not in the comparator, it is in **how the comparator is used**:
`List.sort` only reads it as a sorting rule, and a return of `0` deletes no element at all, it
only leaves two elements next to each other. `TreeSet` reads that same return of `0` as a
**uniqueness decision.** The same object, the same method, the same return value — two
separate containers count it as answering two separate questions. What produces the flaw is
not the comparator itself, it is the container that uses it as a uniqueness decision.

This last measurement ties together all the lesson's threads. Writing a comparator is not, on
its own, a flaw; the flaw surfaces depending on which container's hand that comparator is in,
answering which question. `List.sort` only asks it "which one should come first" and reads a
return of `0` as "both can stand in the same place." `TreeSet` reads that same return of `0` as
"only one of these can exist." A comparator is a single method and carries a single decision;
what decides which question it will be counted as answering is the container the caller
chooses.

#### Summary

- Natural order sits in a type's own `compareTo` and is part of the type's definition; a
  comparator is a separate object the caller writes at the call site, never touching the
  class. The same data can be sorted in two separate orders.
- Stable sorting is `List.sort`'s own promise: it produces a byte-for-byte identical result in
  `ArrayList` and `LinkedList`, without disturbing the relative order of elements with equal
  keys.
- The caller's rule: if a comparator is inconsistent with `equals`, two elements that return
  `0` in a sorted set collapse into a single element — the flaw arises with no exception at
  all, silently.
- A non-transitive comparator silently produces a wrong order on small input, and on large
  enough input runs into the sorting algorithm's own consistency check and falls by name; the
  threshold depends on the data.
- Bounding measurement: the same inconsistent comparator loses no element in a list. What
  produces the flaw is not the comparator itself, it is the container that reads its return
  value as a uniqueness decision.

#### Next Step

Everything measured in this topic showed that the ordering rule no longer sits inside the
container, it sits in an object given from outside — both `Comparator` and a type
implementing `Comparable` were small contracts written independently of the container itself.
The next topic looks at that object itself: what do the library's single-method types
(functional interfaces), including `Comparator`, promise, and when that promise is written as
a lambda at the call site, which guarantee stays in place, which one disappears?

## Functional Java

The promise single-abstract-method types carry as an interface, lambda expressions' capture rule, what the stream API's intermediate and terminal operations guarantee and what they do not, collectors' grouping and reducing behavior, expressing absence as a type.

### Functional Interfaces

Source: https://academia.sh/en/courses/java-standard-library/functional-interfaces

Single abstract method is a compile-time promise: a two-method interface can never be a lambda target, and the marker only catches a definition that breaks this promise early. The interface's second promise — behavior — gets checked nowhere: the type promise gets compiled in all four members of the family, while the purity and no-side-effects promise can break silently in Predicate and Function.

The previous topic's last lesson moved the ordering rule to an
externally supplied object: a comparator was a single-method type the
caller attached to the collection. The library called it, it never made
the decision. This lesson looks at that object itself — a type with a
single abstract method, a **functional interface** — measures what it
tells the compiler, and which of its promises no one checks.

Every line of the standard library is a promise, and one of three
sources gives that promise: the **interface** itself, the chosen
**implementation**, or a rule the **caller** follows. Functional
interfaces are this trio's narrowest example — they carry only one
abstract method — and it is exactly this narrowness that lets them be
the target of lambda expressions. Does the compiler hold this
narrowness, or does it only sit in documentation? The interface carries
a second promise too: how the method is supposed to **behave**. Where
does that promise sit, who checks it?

#### Single Abstract Method Is a Compile-Time Promise

- **FJ1** — Four container forms get tried: a single-method and a
  two-method interface, each both marked and unmarked with
  `@FunctionalInterface`. The source texts get written within the
  lesson.
- **FJ2** — Whether an interface's definition compiles gets measured
  separately from whether a class using that interface as a lambda
  target compiles; the two do not get mixed.
- **FJ3** — The compiler's error message text does not get printed;
  only whether it compiles gets reported, because what gets measured
  is not the message's content, it is whether the gate is open.

```java
// Compile.java - shared core: does the given source text compile
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.file.*;
import java.util.spi.ToolProvider;

class Compile {
    static boolean compiles(String source, String className) throws Exception {
        Path dir = Files.createTempDirectory("scale");
        Path file = dir.resolve(className + ".java");
        Files.writeString(file, source);
        PrintWriter silent = new PrintWriter(Writer.nullWriter());
        return ToolProvider.findFirst("javac").orElseThrow()
                .run(silent, silent, "-d", dir.toString(), file.toString()) == 0;
    }
}
```

```java
// Marker.java - what the @FunctionalInterface marker catches
public class Marker {
    static final String SINGLE = "interface Operation { int apply(int x); }";
    static final String SINGLE_MARKED = "@FunctionalInterface\ninterface Operation { int apply(int x); }";
    static final String TWO = "interface Operation { int apply(int x); int revert(int x); }";
    static final String TWO_MARKED = "@FunctionalInterface\ninterface Operation { int apply(int x); int revert(int x); }";

    static final String[][] ATTEMPTS = {
        {"single method, unmarked", SINGLE},
        {"single method, marked", SINGLE_MARKED},
        {"two methods, unmarked", TWO},
        {"two methods, marked", TWO_MARKED},
    };

    public static void main(String[] args) throws Exception {
        System.out.printf("%-24s %-20s %s%n", "form", "definition compiles", "can be a lambda target");
        for (String[] d : ATTEMPTS) {
            String label = d[0], body = d[1];
            boolean definition = Compile.compiles(body, "Operation");
            boolean lambda = definition && Compile.compiles(
                    body + "\nclass Use { Operation i = x -> x; }", "Operation");
            System.out.printf("%-24s %-20s %s%n", label,
                    definition ? "compiled" : "did not compile",
                    !definition ? "-" : (lambda ? "compiled" : "did not compile"));
        }
    }
}
```

```
form                     definition compiles  can be a lambda target
single method, unmarked  compiled             compiled
single method, marked    compiled             compiled
two methods, unmarked    compiled             did not compile
two methods, marked      did not compile      -
```

Only one of the four rows falls at the definition stage: the
two-method, marked interface never compiles at all, because the marker
tells the compiler "this interface should have exactly one abstract
method," and the compiler checks that. The other three rows'
definitions compile — an unmarked two-method interface is valid as an
ordinary interface too; Java does not forbid an interface from having
more than one abstract method.

The real distinction is in the second column. Both single-method
interfaces — marked or not — can be a lambda target. The two-method,
unmarked interface, even though its definition passes, **cannot** be a
lambda target: the expression `x -> x` does not say which method it
corresponds to, and the compiler cannot resolve it. What the marker
does and does not do splits apart right here.

#### The Marker Adds No Guarantee, It Catches Early

The table confirms a single conclusion: an interface being able to be
a lambda target is a **structural** property — carrying exactly one
abstract method — and this property exists or does not, independent of
the marker. The two-method, unmarked interface compiles as a
definition too, but it was never functional to begin with; no one just
said so, until someone tries to use it with a lambda. The error at
that moment comes up far from the line where the interface got
defined, separately at every point it gets used.

The marker closes this distance. The two-method, marked interface
falls **at compile time** — without waiting for use, at the definition
itself. The promise the marker gives is not "this interface is
functional," it is "tell me here if this interface stops being
functional." The **interface** was already giving the guarantee
through its own structure; the marker only moves the moment that
guarantee breaks from the use site to the **definition site**. Types
like `Runnable`, `Comparator`, `Predicate` in the standard library
carry this marker, but even if you removed the marker, they would
still be single-abstract-method interfaces and would still be lambda
targets — the marker does not make them functional, it only catches
their breakage early.

The Object-Oriented Java course counted four things the compiler
rejects: access modifiers, `final`, checked exceptions, type
parameters. The single-abstract-method rule is not a fifth protection
added to this list, it is the **same** protection carried into the
standard library — here too, the compiler refuses a structure and
reports it at the definition line. But this protection's scope is
narrow: it looks only at the interface's **shape**, at how many methods
there are. It never looks at what the promise behind the shape is —
and this distinction shows up in the measurement below.

The shape itself carries less than it might seem to. In the
Object-Oriented Java course, a class implementing an interface got
built **by name**: the interface's name appears in the `implements`
line. A lambda expression never writes this name at all; the compiler
looks not at the target's name, only at its **shape**.

```java
// Structural.java - can the same lambda be the target of two interfaces unaware of each other
public class Structural {
    static final String TWO_INTERFACES = """
        interface Multiplier { int apply(int x); }
        interface Converter { int apply(int x); }
        class Use {
            Multiplier m = x -> x * 3;
            Converter c = x -> x * 3;
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.println("two separate interfaces unaware of each other, same-bodied lambda: "
                + (Compile.compiles(TWO_INTERFACES, "Multiplier") ? "compiled" : "did not compile"));
    }
}
```

```
two separate interfaces unaware of each other, same-bodied lambda: compiled
```

`Multiplier` and `Converter` do not know each other at all; there is no
inheritance between them, no shared supertype — both only carry one
abstract method of the same shape. The same lambda expression compiles
for both. This differs from the behavior of a class implementing an
interface: there, the name is binding; here, the shape is enough. The
promise the compiler checks is exactly this narrow: not name, not
lineage, only parameter count, parameter types, and return type.

#### Default, Static, and Object Methods Do Not Count

The single-abstract-method rule does not say "single method," it says
"single **abstract** method." An interface can carry methods with a
default body (`default`) or static methods; these do not count as
abstract and do not enter the count — because the caller does not have
to implement them, the library has already written them.

- **FJ4** — Two containers get tried: one adds a default and a static
  method next to an abstract method; the other redeclares two of
  Object's own methods (`equals`, `toString`) as abstract next to an
  abstract method. Both are marked with `@FunctionalInterface`.
- **FJ5** — For both containers, whether the definition compiles and
  whether it can be a lambda target get measured separately.

```java
// DoesNotCount.java - default, static, and Object methods do not enter the abstract count
public class DoesNotCount {
    static final String MULTI_MEMBER = """
        @FunctionalInterface
        interface Operation {
            int apply(int x);
            default int doubled(int x) { return apply(x) * 2; }
            static Operation identity() { return x -> x; }
        }
        """;

    static final String OBJECT_SIGNED = """
        @FunctionalInterface
        interface Operation {
            int apply(int x);
            boolean equals(Object o);
            String toString();
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.println("one abstract + two default/static methods : "
                + (Compile.compiles(MULTI_MEMBER, "Operation") ? "compiled" : "did not compile"));
        System.out.println("one abstract + two of Object's methods    : "
                + (Compile.compiles(OBJECT_SIGNED, "Operation") ? "compiled" : "did not compile"));

        boolean lambdaMultiMember = Compile.compiles(
                MULTI_MEMBER + "\nclass K { Operation i = x -> x; }", "Operation");
        boolean lambdaObjectSigned = Compile.compiles(
                OBJECT_SIGNED + "\nclass K { Operation i = x -> x; }", "Operation");
        System.out.println("first can be a lambda target               : "
                + (lambdaMultiMember ? "compiled" : "did not compile"));
        System.out.println("second can be a lambda target              : "
                + (lambdaObjectSigned ? "compiled" : "did not compile"));
    }
}
```

```
one abstract + two default/static methods : compiled
one abstract + two of Object's methods    : compiled
first can be a lambda target               : compiled
second can be a lambda target              : compiled
```

All four lines compile. The first container gives an expected result:
`doubled` and `identity` are not abstract, they have bodies, they do
not enter the count — the interface is still single-abstract-method,
and the marker raises no objection. The second container is less
intuitive: even though `equals` and `toString` get redeclared as
abstract, the compiler does not count them, because both match a
**public** method of the `Object` class, and every implementation
already provides these methods indirectly by inheriting from `Object`.
The rule does not see three abstract declarations, it sees that two of
the three declarations **already have a counterpart in `Object`**, and
counts only `apply`.

This measurement shows the single-abstract-method rule is a slightly
narrower rule than it sounds: what gets counted is not "the count of
abstract declarations in the interface," it is "the count of methods
every implementation would have to **rewrite**." `Object`'s methods
already exist on every class, so they do not enter the rewriting
obligation. A familiar example from the previous topic gets clarified
here: `Comparator<T>` carries around a dozen `default` and `static`
methods (`reversed`, `thenComparing`, `naturalOrder`) and is still
functional, because the only place the caller has to rewrite stays
`compare`. Member count and abstract method count are not the same
thing.

#### The Interface's Behavior Promise Is Checked Nowhere

A functional interface carries two separate promises. The first is
the **signature** — how many parameters, of what type, what it returns
— and this promise gets held at compile time, as measured in the
tables above. The second is **behavior**: `Predicate` expects "give
the same result for the same input," `Function` expects "create no
side effect." Which layer does this second promise sit in?

- **FJ6** — Four members of the standard family get taken as
  examples: `Predicate`, `Function`, `Supplier`, `Consumer`. The rest
  of the family follows the same pattern.
- **FJ7** — A purity violation gets made visible with a call counter,
  a side-effect violation with an accumulator; both are helper state
  written only for this measurement.

```java
// Meaning.java - the interface's type promise and behavior promise are not checked in the same place
import java.util.function.*;

public class Meaning {
    static int callCount = 0;

    public static void main(String[] args) {
        Predicate<Integer> p = x -> x > 0;
        Function<Integer, Integer> f = x -> x * 2;
        Supplier<Integer> s = () -> 42;
        Consumer<Integer> c = x -> { };
        System.out.println("all four type promises matched, all four compiled");
        System.out.println();

        Predicate<Integer> unstable = x -> { callCount++; return callCount == 1; };
        System.out.printf("same input, first call   : %s%n", unstable.test(5));
        System.out.printf("same input, second call  : %s%n", unstable.test(5));

        StringBuilder log = new StringBuilder();
        Function<Integer, Integer> impure = x -> { log.append(x).append(','); return x + 1; };
        impure.apply(1);
        impure.apply(2);
        System.out.printf("did it accumulate a side effect : %s%n", !log.isEmpty());
    }
}
```

```
all four type promises matched, all four compiled

same input, first call   : true
same input, second call  : false
did it accumulate a side effect : true
```

The first line confirms once more where the type promise sits: all
four variables matched their interface's signature, the compiler
checked this, and all four compiled. The remaining three lines test
the second promise. A `Predicate` named `unstable` gives two separate
results on two separate calls with the same input (**5**) — first
`true`, then `false` — and the compiler never got involved, and
runtime never raised an exception either. A `Function` named `impure`
writes to a `StringBuilder` outside its own scope every time it gets
called; this too got blocked neither while compiling nor while
running.

The consequence is **silent**: `Predicate.test` does not throw an
exception because it fails to "give the same result for the same
input," it runs a misbehaving implementation as it is, unchanged. The
family's type promise got confirmed in the first line above: all four
members matched their own signature, all four compiled. The behavior
promise, though, is not distributed evenly across the family. For
`Predicate` and `Function`, purity and no-side-effects are real
documented promises, and both just got broken, both stayed silent —
the compiler did not intervene, runtime raised no exception. For
`Supplier`, the documentation is even looser: there is **no
requirement at all** about producing the same or a different value on
each call, so there is not even a promise to break there. `Consumer`,
on the other hand, is built the opposite way — its purpose is a side
effect to begin with, the documentation does not count that as a
defect, does not treat it as a promise. The one general rule that fits
the table: the compiler keeps the type promise complete in all four
members of the family; the behavior promise is not uniform across the
family, and even where it holds, no layer checks it.

This repeats an observation built in the previous lesson (Comparison
and Sorting), but on a different surface: there, a non-transitive
comparator was measured producing a silently wrong order on a small
input. Here, the same silence shows up on a surface much smaller and
much more frequently used than sorting — a **one-line lambda
expression**. However narrow a functional interface is, that
narrowness does not turn into a behavior guarantee.

#### Summary

- An interface being able to be a lambda target is a structural
  condition: exactly one abstract method. This condition gets held at
  compile time, independent of the marker.
- The `@FunctionalInterface` marker adds no guarantee; it only catches
  this structural condition's violation early, by moving it from the
  use site to the definition site.
- A two-method, unmarked interface compiles too, but cannot be a
  lambda target — not being functional holds without the marker too,
  it just gets noticed later.
- Functional interfaces carry a second promise: behavior (purity, no
  side effects). This promise gets checked neither by the compiler nor
  at runtime; its violation is silent, it raises no exception.
- The standard family's type promise sits with the compiler in all
  four members; the behavior promise is not uniform across the
  family — it can break in `Predicate` and `Function` and stays
  silent when it does, and for `Supplier` such a promise does not even
  exist in the documentation.

#### Next Step

This lesson looked at the functional interface itself: where single
abstract method gets held, where the second promise does not. When a
lambda expression gets written, at that type's site it does one more
thing: it **captures** the names around it. The next lesson measures
what gets captured when it is written at that site — is capturing a
local variable bound to the same constraint as capturing a field, and
do the two produce the same result?

### Lambda Expressions

Source: https://academia.sh/en/courses/java-standard-library/lambda-expressions

When a lambda expression captures a local variable around it, that variable being effectively final is mandatory at compile time; when it captures a field, no such constraint exists, and the same lambda gives a different result once the object changes later. What gets frozen in a captured local variable is its value, not the object it points to.

The previous lesson looked at the functional interface itself: it
measured that single abstract method gets held at compile time, that
the behavior promise gets held nowhere. The moment a lambda expression
gets written, one more thing happens at that type's site — the
expression refers to names outside its own body, and **captures**
those names. Is capturing a local variable bound to the same
constraint as capturing a field, and do the two produce the same
result?

This lesson answers this question directly with a run: two lambdas
with the same body, doing the same work — one reading a local
variable, the other a field — produce how many distinct results across
two separate calls? The concept of **closure** itself — a function
carrying its own defining environment along with it — was built in
the Programming Fundamentals and Objects and Functions in JavaScript
courses and does not get reopened here. What gets measured is not what
a closure is, it is what constraint Java bounds it with.

#### A Lambda Captures a Variable by Its Value

- **FJ8** — The same multiplication gets written with two separate
  `IntUnaryOperator`s: one reads a local variable, the other a static
  field. The factor is 2 when both get built.
- **FJ9** — After the lambda gets built, the field gets changed to 5,
  then to 7; the local variable never gets touched — it cannot be
  touched at all, and the section below measures why.
- **FJ10** — Both lambdas get read twice with the same
  `applyAsInt(10)` call; the returned values get compared directly.

```java
// Capture.java - variable or field: which one does the lambda freeze
import java.util.function.IntUnaryOperator;

public class Capture {
    static int factor = 2;

    public static void main(String[] args) {
        int localFactor = 2;
        IntUnaryOperator localCapturer = x -> x * localFactor;
        System.out.println("local capture, first call    : " + localCapturer.applyAsInt(10));
        factor = 5;
        System.out.println("local capture, field changed : " + localCapturer.applyAsInt(10));

        IntUnaryOperator fieldCapturer = x -> x * factor;
        System.out.println("field capture, factor=5      : " + fieldCapturer.applyAsInt(10));
        factor = 7;
        System.out.println("field capture, factor=7      : " + fieldCapturer.applyAsInt(10));
    }
}
```

```
local capture, first call    : 20
local capture, field changed : 20
field capture, factor=5      : 50
field capture, factor=7      : 70
```

`localCapturer` gives **20** on both calls — even though the static
field changed to 5 in the line between them. The moment the lambda got
built, it took `localFactor`'s value **at that moment** and
**embedded** that value inside itself; whatever happens outside, it
reads this copy. `fieldCapturer`, though, gives two separate results
on two calls: **50**, then **70**. This lambda did not embed a value,
it embedded a **path** to the `factor` field, and reads the current
value through that path on every call.

The same body shape — `x -> x * something` — produces a different
count of results across two separate capture forms: **one** result for
the local variable, **two** separate results for the field. The
question's answer is plain here: capturing a local variable and
capturing a field are not bound to the same constraint, and the two do
not produce the same result.

#### The Effectively-Final Constraint Is at Compile Time

Why the local variable stayed fixed got seen in the previous
measurement; is this fixedness a compiler decision, though, or did it
just come out this way in this example? Java's rule is explicit: if a
lambda expression captures a local variable in the enclosing scope,
that variable has to be **effectively final** — that is, it must
never get reassigned after its first assignment, even if the word
`final` never gets written.

- **FJ11** — The same method body gets tried in two forms: the local
  variable gets assigned once and read by the lambda; in the second
  form, the same variable gets assigned once more after the lambda
  gets built.
- **FJ12** — The error message text the compiler produces does not
  get printed, only whether it compiles gets reported.

```java
// Compile.java - shared core: does the given source text compile
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.file.*;
import java.util.spi.ToolProvider;

class Compile {
    static boolean compiles(String source, String className) throws Exception {
        Path dir = Files.createTempDirectory("scale");
        Path file = dir.resolve(className + ".java");
        Files.writeString(file, source);
        PrintWriter silent = new PrintWriter(Writer.nullWriter());
        return ToolProvider.findFirst("javac").orElseThrow()
                .run(silent, silent, "-d", dir.toString(), file.toString()) == 0;
    }
}
```

```java
// EffectivelyFinal.java - why reassigning the local variable breaks the lambda
public class EffectivelyFinal {
    static final String BASE = """
        import java.util.function.IntSupplier;
        class Trial {
            static IntSupplier make() {
                int counter = 0;
                IntSupplier s = () -> counter;
                %s
                return s;
            }
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.println("unchanged local              : "
                + (Compile.compiles(BASE.formatted(""), "Trial") ? "compiled" : "did not compile"));
        System.out.println("reassignment after lambda    : "
                + (Compile.compiles(BASE.formatted("counter = 1;"), "Trial") ? "compiled" : "did not compile"));
    }
}
```

```
unchanged local              : compiled
reassignment after lambda    : did not compile
```

The second form, where the `counter` variable gets one more assignment
**after** the lambda gets built, does not compile — and this is an
error that falls at **compile time**, not runtime. Whether the
source's `counter = 1;` line would run before or after the lambda gets
called does not matter; the compiler does not execute the control
flow, it only looks at whether there is **more than one assignment**
within the variable's scope. If there is one assignment, the variable
is effectively final and can get captured; if a second assignment
shows up — even one that control flow would never reach — the capture
does not compile.

This is the same pattern as the previous lesson: here too, the
compiler looks at a **structure**, not a behavior. It counts not
whether the local variable actually changes, but how many times it
gets assigned in the source. There is no such count for a field — the
`factor` field can get reassigned as many times as wanted, and the
compiler raises no objection, because capturing a field does not
produce a copy the way capturing a local variable does. The
constraint's reasoning follows from this too: a local variable gets
erased from the stack once the method returns, but the lambda can live
on past that; the compiler resolves this conflict by copying the
variable's value and embedding it inside the lambda itself, and to
keep that copy consistent, it requires that variable to stay
single-assignment. A field, though, does not live on the stack, it
lives in the object or the class itself; the lambda keeps track of
which object it belongs to, and no copy needs to be made.

#### The Same Name Is Two Separate Variables in Classic and Enhanced Loops

The effectively-final constraint's most visible consequence shows up
in loops. The **enhanced** `for` loop (`for (String s : list)`) and
the **classic** `for` loop (`for (int i = 0; ...; i++)`) look like
they do the same work, but once a lambda wants to capture the loop
variable, the two come apart.

- **FJ13** — A three-item list gets iterated with an enhanced for; the
  lambda built in every iteration adds that iteration's variable to a
  list. Once the loop finishes, every lambda in the list gets called.
- **FJ14** — The same attempt gets repeated with a classic for and an
  incrementing counter; what is expected here is not a runtime
  result, it is whether it compiles.

```java
// Loop.java - are the classic and enhanced for loops the same for lambda capture
import java.util.*;
import java.util.function.Supplier;

public class Loop {
    public static void main(String[] args) {
        List<String> source = List.of("pear", "cherry", "apple");
        List<Supplier<String>> capturers = new ArrayList<>();
        for (String s : source) {
            capturers.add(() -> s);
        }
        System.out.print("enhanced for, three lambdas' result: ");
        for (Supplier<String> c : capturers) System.out.print(c.get() + " ");
        System.out.println();
    }
}
```

```
enhanced for, three lambdas' result: pear cherry apple 
```

Three lambdas give three separate values: `pear`, `cherry`, `apple`.
This looks, at first glance, like it contradicts the "capture freezes
the value" rule — was the loop variable `s` not reassigned three
times? No: the enhanced `for` builds a **new** local variable named
`s` on every iteration; that single-assignment variable gets replaced
by another variable on the next iteration, the same variable never
gets reassigned. Three lambdas captured three separate `s`'s, and that
is why three separate values got frozen.

```java
// ClassicLoop.java - can the counter variable be captured in a classic for
public class ClassicLoop {
    static final String SOURCE = """
        import java.util.*;
        import java.util.function.IntSupplier;
        class Trial {
            static void make() {
                List<IntSupplier> capturers = new ArrayList<>();
                for (int i = 0; i < 3; i++) {
                    capturers.add(() -> i);
                }
            }
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.println("classic for, can i be captured   : "
                + (Compile.compiles(SOURCE, "Trial") ? "compiled" : "did not compile"));
    }
}
```

```
classic for, can i be captured   : did not compile
```

In the classic `for` loop, the situation is exactly the opposite. `i`
is a **single** variable, and `i++` reassigns it on every iteration;
three iterations mean three separate assignments on the same `i`.
This directly breaks the effectively-final definition, and compilation
stops at the line where the lambda tries to capture `i` — the exact
same rule as the `counter = 1;` example measured earlier, here inside
a loop body. Even though the two loops look syntactically similar, the
enhanced `for` builds a new name every turn while the classic `for`
reassigns a single name over and over — and lambda capture is exactly
what brings this difference out.

#### Four Syntaxes, the Same Capture Rule

A lambda expression's body can be a single expression, it can be a
block in curly braces, its parameter type can get written explicitly
or not at all — and if a method already exists that does the same
work, the lambda can get specified without writing a lambda at all,
with a **method reference** (`Class::method`). These four forms are
separate syntaxes; as long as the target interface is the same, all
four go through the same compile path.

- **FJ15** — The same `Function<Integer, Integer>` gets written in
  four separate forms and gets called with the same input (`6`).

```java
// Syntax.java - four writings, same result
import java.util.function.Function;

public class Syntax {
    static int square(int x) { return x * x; }

    public static void main(String[] args) {
        Function<Integer, Integer> expression = x -> x * x;
        Function<Integer, Integer> bodied = x -> { return x * x; };
        Function<Integer, Integer> typed = (Integer x) -> x * x;
        Function<Integer, Integer> methodReference = Syntax::square;

        System.out.println("expression form      : " + expression.apply(6));
        System.out.println("body form             : " + bodied.apply(6));
        System.out.println("explicit type         : " + typed.apply(6));
        System.out.println("method reference      : " + methodReference.apply(6));
    }
}
```

```
expression form      : 36
body form             : 36
explicit type         : 36
method reference      : 36
```

All four lines give the same result, because all four bind to the
same target — `Function<Integer, Integer>`'s single abstract method.
The **body** form requires the word `return` and allows more than one
statement; the **expression** form implicitly returns a single value.
`typed` writes its parameter type explicitly; `expression` and
`bodied` infer this type from the target interface. `methodReference`
writes no lambda at all — it points, by name, to the already existing
`square` method.

A method reference is not exempt from the capture rule, it just may
have nothing to capture. `Syntax::square`, pointing to a static
method, captures nothing, because a static method has no instance it
is bound to. But a reference pointing to an **instance** method
through that instance — something like `list::size` — captures the
`list` variable exactly the way the lambda `() -> list.size()` does,
and is subject to the same constraints, the same result; both writings
go through the same compile step, one is only a shorthand for the
other. Syntax changes, but the capture rule does not — the rule's
condition is not the lambda keyword, it is reading a name from the
enclosing scope.

#### The Frozen Value, the Unfrozen Object

The measurement so far is open to a misreading: the sentence "the
local variable is fixed" might give the impression that the captured
object stays fixed too. The boundary measurement separates this.

- **FJ16** — A list variable gets assigned once and never reassigned
  — satisfying the effectively-final rule. The lambda reads the
  list's **size**, not the list itself.

```java
// Reference.java - a local variable freezes its value, not the object
import java.util.*;
import java.util.function.Supplier;

public class Reference {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(List.of("alpha"));
        Supplier<Integer> sizeReader = () -> list.size();
        System.out.println("size when lambda built : " + sizeReader.get());
        list.add("beta");
        list.add("gamma");
        System.out.println("size after list changes: " + sizeReader.get());
    }
}
```

```
size when lambda built : 1
size after list changes: 3
```

The `list` variable got assigned once and never reassigned again; the
effectively-final constraint held from start to end. Despite this,
`sizeReader.get()` gives two separate numbers on two calls: **1**,
then **3**. The contradiction is only apparent; what the constraint
froze was the `list` variable **itself** — that is, which object it
points to — not that object's **content**. `list` still points to the
same `ArrayList` object, but two more items got added inside that
object, and the lambda reads the current content on every call.

This bounds the previous section's observation that "local capture
stays fixed": what stays fixed is the **reference**, not the mutable
object the reference points to. The effectively-final rule gives no
**immutability** guarantee; it only requires that a **name** point to
the same object throughout the flow. The immutable string measured in
the Primitive Types and Wrappers lesson and the mutable list captured
here, for this reason, behave separately — in the first, the object
itself can never change; in the second, the object can change **in
place**, and capture does not stop this.

#### Summary

- When a lambda expression captures a local variable, it copies that
  variable's value and embeds it inside itself; when it captures a
  field, it makes no copy, it keeps the path to the field.
- Two lambdas with the same body produce a different count of results
  across two capture forms: one unchanging result in local capture,
  separate results changing with the field in field capture.
- A captured local variable being effectively final is mandatory at
  compile time; a second assignment to the same variable, even after
  the lambda gets built, stops compilation.
- No such constraint exists for field capture; a field can get
  reassigned after the capture date too, and the compiler does not
  intervene.
- Boundary measurement: the effectively-final constraint freezes not
  the object the variable points to, only the variable itself. If the
  list the captured reference points to changes later, the lambda
  sees the changed content.
- A lambda expression's four syntaxes (expression, body, explicit
  type, method reference) bind to the same target and give the same
  result; a method reference is not exempt from the capture rule, it
  just may have nothing to capture.

#### Next Step

This lesson measured what a single lambda expression takes from its
surroundings, and how the four syntaxes bind to the same target. The
standard library's real form of use is not a single lambda like this,
it is several lambdas chained one after another — a **stream chain**.
Even though every link works correctly on its own, a separate question
is born once it gets strung into a chain: which part of the chain
depends on the source's order, which one gives the same order under
every condition, which one never runs without a terminal operation,
and which rule is left entirely to the caller. The next lesson answers
this question by running it across a stream chain's ten separate
operations.

### Stream API

Source: https://academia.sh/en/courses/java-standard-library/stream-api

Java's stream chain guarantees three of ten operations independent of the source, defers five to the source's order, and leaves two to the caller. An intermediate operation never runs without a terminal operation, but even once a terminal operation gets called, which item gets visited is not guaranteed. Consuming the same stream twice and growing the source while iterating both fall with an exception.

The previous lesson measured what a single lambda expression captures
from its surroundings, and showed that four syntax forms bind to the
same target. The standard library's real form of use is not a single
lambda like this, it is several lambdas chained one after another — a
**stream** chain. Even though every link works correctly on its own, a
separate question is born once it gets strung into a chain: which part
of the chain depends on the source's order, which one gives the same
order under every condition, which one never runs without a terminal
operation, and which rule is left entirely to the caller?

This lesson classifies ten separate claims by running them: for each
one, is the observed behavior independent of the source (an
**interface guarantee**), bound to the source's own order, or a rule
the **caller** follows? The measurement's shape is the same as before
— code gets written, run, the result gets compared — but what gets
compared here is not two implementations, it is the same operation's
behavior on two separate sources. To avoid confusion with `java.io`
streams, every bare "stream" word used from here on gets the qualifier
from this paragraph: the **stream chain**, or stream for short.

#### The Stream Chain's Order Comes from the Source, Not the Interface

- **FJ17** — The same five words get held in two sources: an ordered
  List and an unordered HashSet. The same stream chain runs on both,
  and the results get compared with equals; the raw iteration order
  never gets printed anywhere.
- **FJ18** — `sorted()` runs with natural order; uppercasing happens
  with `Locale.ROOT`.
- **FJ19** — `distinct()` and `findFirst()` get tried on the ordered
  source; they do not get repeated with the unordered source, because
  what gets measured is the existence of first-seen order, and this
  concept is undefined on an unordered source.

```java
// StreamOrder.java - does the stream chain's order come from the source or the interface
import java.util.*;
import java.util.stream.*;

public class StreamOrder {
    public static void main(String[] args) {
        List<String> orderedSource = List.of("pear", "cherry", "apple", "grape", "fig");
        Set<String> unorderedSource = new HashSet<>(orderedSource);

        List<String> sorted1 = orderedSource.stream().sorted().toList();
        List<String> sorted2 = unorderedSource.stream().sorted().toList();
        System.out.println("sorted(), alphabetical order on ordered source : "
                + sorted1.equals(List.of("apple", "cherry", "fig", "grape", "pear")));
        System.out.println("sorted(), same result on unordered source      : " + sorted2.equals(sorted1));

        List<String> expectedUpper = List.of("PEAR", "CHERRY", "APPLE", "GRAPE", "FIG");
        List<String> mappedOrdered = orderedSource.stream().map(x -> x.toUpperCase(Locale.ROOT)).toList();
        List<String> mappedUnordered = unorderedSource.stream().map(x -> x.toUpperCase(Locale.ROOT)).toList();
        System.out.println("map(), keeps input order on ordered source     : " + mappedOrdered.equals(expectedUpper));
        System.out.println("map(), keeps input order on unordered source   : " + mappedUnordered.equals(expectedUpper));

        List<String> expectedLong = List.of("pear", "cherry", "apple", "grape");
        List<String> filtered = orderedSource.stream().filter(x -> x.length() > 3).toList();
        System.out.println("filter(), keeps order on ordered source        : " + filtered.equals(expectedLong));

        List<String> repeated = List.of("pear", "cherry", "pear", "apple", "cherry");
        List<String> unique = repeated.stream().distinct().toList();
        System.out.println("distinct(), kept in first-seen order           : "
                + unique.equals(List.of("pear", "cherry", "apple")));

        Optional<String> first1 = orderedSource.stream().findFirst();
        Optional<String> first2 = orderedSource.stream().findFirst();
        System.out.println("findFirst(), do two calls give the same item   : " + first1.equals(first2));
    }
}
```

```
sorted(), alphabetical order on ordered source : true
sorted(), same result on unordered source      : true
map(), keeps input order on ordered source     : true
map(), keeps input order on unordered source   : false
filter(), keeps order on ordered source        : true
distinct(), kept in first-seen order           : true
findFirst(), do two calls give the same item   : true
```

The first two lines measure a guarantee independent of the source:
`sorted()` produces alphabetical order on the ordered source, and
gives the **same** result on the unordered `HashSet` source too.
Whatever container the input gets held in, `sorted()` fulfills its own
promise on its own — this is an **interface guarantee**, and it needs
nothing from the source's own order.

The third and fourth lines show exactly the opposite. `map()` carries
the input's order on the ordered `List` source (`true`), but does not
carry that order on the unordered `HashSet` source, even though it
works on the same words (`false`). `map()` itself never changed; what
changed was the **source**. The order promise belongs not to `map()`,
but to the **container underneath it** — this is the lesson's boundary
measurement, and it confirms the topic's claim: the sentence "a stream
keeps order" is the source's promise, not the stream's.

The remaining three lines fall into the same **source-dependent** set.
`filter()` keeps the order of the remaining items on the ordered
source; `distinct()` keeps the first-seen one among duplicates and
does not disturb the order; `findFirst()` gives the same item on two
separate calls on the ordered source. What the three share: what
decides the result is the source's **encounter order**, and this order
exists only when the source is ordered. Repeated with an unordered
source, the concept of "first seen" or "first item" itself becomes
undefined.

This lesson uses "source-dependent" as a separate bucket, because the
trio's middle term from the collections topic — **implementation
behavior** — does not correspond to a different choice here. There,
what got chosen was a separate class, like `HashMap` versus `TreeMap`;
here, `map()` itself never changes, the only thing that changes is the
source's **structure** (ordered or not). The outcome comes out the
same direction: the observation is in the hands of a third party
invisible in the call's writing, and that party in this topic is not
the interface, it is the **source**.

The same row also has a looser end. `findFirst()` says "give the same
item on an ordered source"; the neighboring method `findAny()` gives
no such promise, it only guarantees returning **an** item that
matches.

- **FJ20** — `findFirst()` and `findAny()` get called separately on
  the same ordered source, from the same stream chain, and the
  returned values get compared with equals.

```java
// FindAny.java - do findFirst and findAny give the same promise
import java.util.*;

public class FindAny {
    public static void main(String[] args) {
        List<String> source = List.of("pear", "cherry", "apple", "grape", "fig");
        Optional<String> first = source.stream().findFirst();
        Optional<String> any = source.stream().findAny();
        System.out.println("findFirst() result: " + first.get());
        System.out.println("findAny() result  : " + any.get());
        System.out.println("are the two equal : " + first.equals(any));
    }
}
```

```
findFirst() result: pear
findAny() result  : pear
are the two equal : true
```

In this ordered, single-threaded stream chain, the two give the same
item — the API's looseness never shows up here, because putting that
looseness to actual use only comes up once the stream gets
**parallelized**, and this course does not measure stream
parallelization. The only thing measured is this: two separate methods
giving the same result with two separate promises shows that a
promise's tightness can be independent of the outcome — `findAny()`
promises less, but we do not pay this lesser promise's cost in this
run. `findAny()` does not enter the table, because the one promise it
gives — "a matching item" — is already unconditional; the only thing
bound to the source is `findFirst()`'s "**which** item" question.

#### An Intermediate Operation Never Runs Without a Terminal

Intermediate operations in a stream chain, like `map`, `filter`,
`sorted`, do not run the moment they get built; they only get added to
the chain. This is the here-asked form of **lazy evaluation**, a
concept built in a previous topic — the concept does not get
reopened, the question asked is this: is this laziness the stream
chain's **promise**, or is it only an implementation detail?

- **FJ21** — The same intermediate operation gets built three times:
  with no terminal at all, with the `count()` terminal, and with the
  `toList()` terminal. Each time, a counter counts how many times the
  intermediate operation ran.

```java
// Laziness.java - does an intermediate operation run without a terminal operation
import java.util.*;
import java.util.stream.*;

public class Laziness {
    public static void main(String[] args) {
        List<String> source = List.of("pear", "cherry", "apple", "grape", "fig");

        int[] counter1 = {0};
        Stream<String> pipeline = source.stream().peek(x -> counter1[0]++);
        System.out.println("steps run with no terminal called          : " + counter1[0]);

        int[] counter2 = {0};
        long length = source.stream().peek(x -> counter2[0]++).count();
        System.out.println("steps run with the count() terminal        : " + counter2[0] + " (length=" + length + ")");

        int[] counter3 = {0};
        source.stream().peek(x -> counter3[0]++).toList();
        System.out.println("steps run with the toList() terminal       : " + counter3[0]);
    }
}
```

```
steps run with no terminal called          : 0
steps run with the count() terminal        : 0 (length=5)
steps run with the toList() terminal       : 5
```

The first line gives the expected result: when no terminal ever gets
called, the intermediate operation never runs, the counter stays at
zero. This is an unconditional guarantee — the stream chain's own
promise — and this is the extent of lazy evaluation's counterpart here
from the fourth topic.

The second line narrows this guarantee in one direction. Even though
`count()` gets called, the counter is still **zero**: the length got
computed correctly (**5**), but the intermediate operation never ran.
The reason is in the source itself: the list `List.of(...)` produces
already knows its size, and once an intermediate operation that does
not change item count, like `map`/`filter`, gets added to the chain,
the stream chain can compute the result straight from the source's
size without visiting items one by one. Since `peek` is part of this
chain too, this computation that takes a shortcut through the source's
size never calls it at all. The third line confirms this: `toList()`
genuinely needs every item to produce its result, so there is no
shortcut, and the intermediate operation runs **five** times.

The conclusion here is narrower than it looks at first glance: what
gets guaranteed is "never running without a terminal," not "running
for every item once a terminal gets called." The second is an
**implementation decision** — if the stream chain can produce the
result another way, it can skip the intermediate step. `peek`, for
this reason, is only for observation and debugging; if it gets built
as part of business logic, whether it runs stays dependent on the
terminal.

#### A Stream Is Single-Use, and Cannot Grow Its Source While Iterating

The last two rows fall into the **caller's rule**: these two are
constraints the caller has to follow, independent of the source's type
or the stream chain's shape.

- **FJ22** — A stream object gets consumed twice; the second
  consumption gets tried in a separate `try` block. In a separate
  stream chain, an item gets added to the source list during
  iteration.

```java
// Rule.java - the caller's two rules: single use and no growing while iterating
import java.util.*;
import java.util.stream.*;

public class Rule {
    public static void main(String[] args) {
        List<String> source = List.of("pear", "cherry", "apple", "grape", "fig");

        Stream<String> singleUse = source.stream();
        singleUse.count();
        try {
            singleUse.count();
            System.out.println("consuming the same stream a second time: fell through");
        } catch (IllegalStateException e) {
            System.out.println("consuming the same stream a second time -> exception: " + e.getClass().getSimpleName());
        }

        List<String> growing = new ArrayList<>(source);
        try {
            growing.stream().forEach(x -> {
                if (x.equals("apple")) growing.add("mango");
            });
            System.out.println("growing the source while iterating: fell through, size=" + growing.size());
        } catch (ConcurrentModificationException e) {
            System.out.println("growing the source while iterating -> exception: " + e.getClass().getSimpleName());
        }
    }
}
```

```
consuming the same stream a second time -> exception: IllegalStateException
growing the source while iterating -> exception: ConcurrentModificationException
```

Both rules give an **exception** when they fall, they do not stay
silent. Once `count()` gets called once on `singleUse`, the stream
counts as consumed; the second call fails with `IllegalStateException`
because it tries to reuse the stream itself — a rule that comes from
the stream object's own state, independent of the source's type. This
behavior shows that a stream object internally carries a flag marked
"consumed": once that flag flips, calling a new terminal on it —
whichever one — gets met with the same exception.

The second rule rests on a different mechanism: while the `growing`
list gets iterated with `forEach`, the same list gets grown, and this
breaks the list's own modification counter, raising
`ConcurrentModificationException` — even though the stream's terminal
is `forEach`, the exception that falls is actually the **source's
own** protection; the stream chain here is only an intermediary using
the source's iterator. The difference between these two matters: the
first exception is the stream API's **own** rule and is independent of the
source's type; the second is bound to the stream's **source** — a
stream chain built from an unmodifiable source can never grow while
iterating in the first place, so this rule never enters play there at
all.

The ten rows measured across this lesson can get gathered in a single
table:

| # | Measured | Result | Source |
|---|---|---|---|
| 1 | `sorted()`, ordered source | alphabetical order | interface guarantee |
| 2 | `sorted()`, unordered source | same result | interface guarantee |
| 3 | `map()`, ordered source | order kept | source-dependent |
| 4 | `map()`, unordered source | order not kept | source-dependent |
| 5 | `filter()`, ordered source | order kept | source-dependent |
| 6 | `distinct()`, ordered source | first-seen kept | source-dependent |
| 7 | `findFirst()`, ordered source | consistent result | source-dependent |
| 8 | intermediate operation, no terminal | 0 runs | interface guarantee |
| 9 | second consumption of the same stream | exception | caller's rule |
| 10 | growing the source while iterating | exception | caller's rule |

The distribution is this: **three** are a guarantee independent of the
source, **five** depend on the source's or the terminal's choice,
**two** are rules the caller has to follow and that fall with an
exception when broken. `count()` skipping `peek` did not enter the
table as a separate row, because what it measures is not a source
dependency, it is the terminal's own choice — it should get read as a
lower bound of row eight. The majority of the ten operations — **five**
— carry the promise not of the interface, but of the **source**; this
runs the same direction as the order the previous topic built: most of
the standard library's visible behavior is the promise not of the
interface it speaks through, but of the choice underneath it.

#### Summary

- A stream chain's promise about order usually comes from the source:
  `map`, `filter`, `distinct`, `findFirst` keep encounter order only
  if the source is ordered.
- `sorted()` is a guarantee independent of the source: it produces the
  same result with an ordered or unordered source. Boundary
  measurement: the sentence "a stream keeps order" is mostly the
  source's promise.
- An intermediate operation never runs without a terminal operation;
  this is an unconditional guarantee. But visiting every item once a
  terminal gets called is not guaranteed — `count()` can skip the
  intermediate operation entirely, `toList()` cannot.
- Consuming the same stream a second time falls with
  `IllegalStateException`; this is a rule belonging to the stream
  object's own state, independent of the source's type.
- Growing the source collection while iterating the stream falls with
  `ConcurrentModificationException`; the exception that falls is not
  the stream's protection, it is the underlying collection's.
- Three of the ten measurements are a guarantee independent of the
  source, five are source-dependent, two are the caller's rule; the
  majority stayed source-dependent.

#### Next Step

This lesson measured what a stream chain says, and what it does not:
order is mostly the source's promise, laziness is an unconditional
guarantee but visiting every item is not, and two rules — single use,
no growing while iterating — fall directly to the caller. What
accumulates at the end of the chain is usually not a single value, it
is a **container** — a list, a map, a number — and this lesson always
used ready-made collecting methods (`toList()`, `count()`). The next
lesson looks at the piece that builds that container: what promise a
**collector** gives, whether the result container's type is the
interface's promise, and, once a repeated key gets encountered, which
collector throws an exception and which one merges.

### Collectors

Source: https://academia.sh/en/courses/java-standard-library/collectors

A collector gets built from four parts: supplier, accumulator, combiner, finisher. The result container's type and mutability are not the interface's promise — toList() returns a mutable list but promises this nowhere, while toUnmodifiableList() guarantees immutability. Once a duplicate key gets encountered, the map collector gives an exception, grouping merges.

The previous lesson measured what a stream chain says: order is
mostly the source's promise, laziness is an unconditional guarantee
but visiting every item is not. What accumulates at the end of the
chain is usually not a single value, it is a **container** — a list,
a map, a number — and the piece that builds this container is a
**collector** (`Collector`) handed to the `collect` call. Names like
`toList()`, `groupingBy()`, `joining()` read less like a method and
more like a **recipe**: each is a collector the library has already
built and delivers ready-made. What promise does a collector itself
give, is the result container's type part of that promise, and once a
duplicate key gets encountered, what does each collector do?

#### A Collector's Four Parts

A collector is not a single method; it gets built from four separate
parts. The **supplier** builds an empty container, the **accumulator**
works each item in the stream into this container, the **combiner**
merges two separate containers into one (it enters play in parallel
runs), the **finisher** turns the accumulated container into the final
result. The ready-made collectors in the `Collectors` class already
deliver these four parts built; here, they get built by hand to see
the parts themselves.

- **FJ23** — The four parts get given one by one with `Collector.of`
  and get used in a stream chain. The container here is not a string,
  it is a single-slot `int[]` — an accumulator holding a total letter
  count.

```java
// Parts.java - a collector's four parts: supplier, accumulator, combiner, finisher
import java.util.function.*;
import java.util.stream.*;

public class Parts {
    public static void main(String[] args) {
        Supplier<int[]> supplier = () -> new int[1];
        BiConsumer<int[], String> accumulator = (box, word) -> box[0] += word.length();
        BinaryOperator<int[]> combiner = (a, b) -> { a[0] += b[0]; return a; };
        Function<int[], Integer> finisher = box -> box[0];

        Collector<String, int[], Integer> letterCounter =
                Collector.of(supplier, accumulator, combiner, finisher);

        int total = Stream.of("pear", "cherry", "apple").collect(letterCounter);
        System.out.println("collector built from four parts, total letters: " + total);
    }
}
```

```
collector built from four parts, total letters: 15
```

The result is correct: `pear` (4) + `cherry` (6) + `apple` (5) =
**15**. But what gets measured here is not the result itself, it is
the four parts' role. `supplier` gets called once before the stream
starts and builds the empty box. `accumulator` gets called once per
item and updates the box — this is the stream chain's real work.
`combiner` never gets called in this run, because the stream is
sequential; no two separate parts came from the stream that need
merging. `finisher` gets called once at the very end and turns the
`int[]` box into a plain `Integer`. Ready-made collectors like
`Collectors.toList()`, `Collectors.groupingBy()` already fill in and
deliver these four parts; the only difference between them is which
container they build and how they write into it.

`combiner` never getting called in this run is not a gap, it is
exactly the expected result. The standard library's four-part design
accounts for the possibility of the stream running **in parallel**
from the start: once the stream gets split into more than one part and
each part accumulates its own box, the step that reduces the boxes to
one is the combiner. This course does not parallelize streams, so the
combiner does no work here — but the collector's signature always
requires it, because the same collector object has to be usable on a
parallel stream too. Three of the four parts (supplier, accumulator,
finisher) are enough on a sequential stream, while the fourth sits
there only for the **possibility**.

#### The Result Container's Type Is Not the Interface's Promise

`Collectors.toList()` returns a `List<T>`, but does not specify which
class's `List` it is. This is the reappearance, here, of the "promise
left to the implementation" pattern measured in the previous topic:
the interface states a **type**, it does not state which concrete
class it is.

- **FJ24** — The same three words get collected with two separate
  collectors: `toList()` and `toUnmodifiableList()`. Adding an item
  afterward gets tried on both.

```java
// Container.java - is the collector's returned container's type and mutability in the docs
import java.util.*;
import java.util.stream.*;

public class Container {
    public static void main(String[] args) {
        List<String> mutable = Stream.of("pear", "cherry").collect(Collectors.toList());
        try {
            mutable.add("apple");
            System.out.println("adding to toList() result: succeeded (no promise in the docs)");
        } catch (UnsupportedOperationException e) {
            System.out.println("adding to toList() result: failed");
        }

        List<String> immutable = Stream.of("pear", "cherry").collect(Collectors.toUnmodifiableList());
        try {
            immutable.add("apple");
            System.out.println("adding to toUnmodifiableList() result: succeeded");
        } catch (UnsupportedOperationException e) {
            System.out.println("adding to toUnmodifiableList() result -> exception: " + e.getClass().getSimpleName());
        }
    }
}
```

```
adding to toList() result: succeeded (no promise in the docs)
adding to toUnmodifiableList() result -> exception: UnsupportedOperationException
```

Adding to the list `toList()` returns **succeeds** — but this is not a
guarantee, it is a consequence of today's implementation.
`Collectors.toList()`'s documentation gives **no promise** about the
returned list's type, mutability, or thread safety; even if this code
returned a different class tomorrow, no contract would break, because
no contract was ever given. `toUnmodifiableList()`, though, does
exactly the opposite: trying to add falls with
`UnsupportedOperationException`, and this fall is a behavior
**explicitly** guaranteed in the documentation. Both methods return a
`List<String>`; their signatures are the same, their promises are not.
This difference cannot be seen by reading the type — only by reading
the documentation, or, exactly as done here, by **running** it.

This ambiguity has a rationale: not promising leaves the library room
to move. Had `toList()`'s documentation fixed which class it would
return, that class could never change again — every change would
break backward compatibility. By not promising, the library keeps the
right to change its own implementation whenever it wants; the price
for this gets paid by the caller having to answer "is this list
mutable" by reading the documentation, not the code.

#### A Finisher Can Get Added to an Existing Collector Later

`toUnmodifiableList()` guaranteed immutability, but it is not the case
that only the one collector carrying its own name can give this
guarantee. Since the four parts are separate from each other, a new
transformation can get added on top of any existing collector's
**finisher** — without touching the rest of the collector.

- **FJ25** — `Collections::unmodifiableList` gets added as a second
  finisher on top of the container `Collectors.toList()` produces.
  `Collectors.joining` also gets tried with three separate
  contributions — delimiter, prefix, suffix — on an empty and a full
  stream.

```java
// Finisher.java - can a finisher be added later, how does a three-part joining work
import java.util.*;
import java.util.stream.*;

public class Finisher {
    public static void main(String[] args) {
        List<String> layered = Stream.of("pear", "cherry", "apple")
                .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
        try {
            layered.add("grape");
            System.out.println("adding to collectingAndThen result: succeeded");
        } catch (UnsupportedOperationException e) {
            System.out.println("adding to collectingAndThen result -> exception: " + e.getClass().getSimpleName());
        }

        String empty = Stream.<String>of().collect(Collectors.joining(", ", "[", "]"));
        String full = Stream.of("pear", "cherry", "apple").collect(Collectors.joining(", ", "[", "]"));
        System.out.println("joining on empty stream : " + empty);
        System.out.println("joining on full stream  : " + full);
    }
}
```

```
adding to collectingAndThen result -> exception: UnsupportedOperationException
joining on empty stream : []
joining on full stream  : [pear, cherry, apple]
```

`Collectors.collectingAndThen` takes an existing collector
(`Collectors.toList()`) and an extra transformation function
(`Collections::unmodifiableList`), and fuses them into a **single new
finisher** — the supplier and accumulator stay exactly the same as
`toList()`'s, only the last step changes. The result is the same as
`toUnmodifiableList()`: adding falls with
`UnsupportedOperationException`. This completes the previous section's
observation that "the result container's type is not the interface's
promise": if the promise is missing, the caller can **add** it
afterward with their own finisher.

The second measurement shows **when** the finisher runs.
`joining(", ", "[", "]")` takes three separate pieces: the delimiter
goes between items, the prefix and suffix get placed **only once**, at
the very start and the very end. Even on an empty stream, the result
is `[]` — the prefix and suffix still get added, because the finisher
adding them runs exactly once at the end, no matter how many items
accumulated. On the full stream, the three items get separated by two
delimiters, and the prefix and suffix again show up only once each.
This is direct proof that the finisher is a step separate from the
accumulator: the accumulator runs once per item, the finisher runs
once, independent of item count.

The delimiter itself shows the same distinction: between three items,
there are **two** delimiters, not three. The logic adding the
delimiter sits in the accumulator and follows the rule "add the
delimiter first, if this is not the first item" — this rule does not
fire on the first item, so it produces one fewer delimiter than the
item count, not one per item. The prefix and suffix, though, have no
place in the accumulator at all; they are only the finisher's job, and
this is why both show up even when the stream is entirely empty.

#### Duplicate Key: Exception in a Map, Merging in Grouping

A collector's rule that actually matters to the caller shows up when
the input arrives in an unexpected shape. When two words of the same
length — that is, the same key — show up, two different collectors
give two different reactions.

- **FJ26** — Two of five words are four letters (`pear`, `plum`), two
  are five letters (`apple`, `grape`). The same stream first gets
  grouped by length, then gets tried for collection into a map with
  length as the key.

```java
// Repeated.java - duplicate key: merges in grouping, exception in a map
import java.util.*;
import java.util.stream.*;

public class Repeated {
    public static void main(String[] args) {
        List<String> words = List.of("pear", "plum", "apple", "grape", "fig");

        Map<Integer, List<String>> grouped = words.stream()
                .collect(Collectors.groupingBy(String::length));
        System.out.println("grouping's map type: " + (grouped instanceof HashMap ? "HashMap" : "other"));
        System.out.println("length 4 group      : " + grouped.get(4));
        System.out.println("length 5 group      : " + grouped.get(5));

        try {
            Map<Integer, String> map = words.stream()
                    .collect(Collectors.toMap(String::length, s -> s));
            System.out.println("toMap duplicate key: fell through, size=" + map.size());
        } catch (IllegalStateException e) {
            System.out.println("toMap duplicate key -> exception: " + e.getClass().getSimpleName());
        }

        Map<Integer, String> mergedMap = words.stream()
                .collect(Collectors.toMap(String::length, s -> s, (a, b) -> a + "+" + b));
        System.out.println("with a merge function given: " + mergedMap.get(4));

        Map<Integer, String> orderedMap = words.stream()
                .collect(Collectors.toMap(String::length, s -> s, (a, b) -> a + "+" + b, TreeMap::new));
        System.out.println("with a supplier given, type: " + (orderedMap instanceof TreeMap ? "TreeMap" : "other"));
    }
}
```

```
grouping's map type: HashMap
length 4 group      : [pear, plum]
length 5 group      : [apple, grape]
toMap duplicate key -> exception: IllegalStateException
with a merge function given: pear+plum
with a supplier given, type: TreeMap
```

The first line repeats the previous section's observation for
`groupingBy`: `groupingBy`'s documentation says "returns a `Map`," it
does not say which class; today's implementation gives a `HashMap`,
but this too is an implementation detail like `toList()`'s, not the
promise itself. `groupingBy(String::length)` splits five words into
two groups, and **no** item gets lost — the length-4 group has two
words, the length-5 group has two words. A duplicate key here is not
an error, it is the **expected** case: grouping already assumes every
key can carry more than one value, and **accumulates** the values in a
list.

`toMap(String::length, s -> s)`, called with the same input, behaves
differently: it assumes every key corresponds to a **single** value,
does not know what to do once a second value arrives, and throws
`IllegalStateException`. The third line shows the path that closes
this ambiguity: `toMap` with three independent arguments takes a
**merge** function, and merges two colliding values into a single
value like `"pear+plum"` — no more exception.

The last line is the boundary measurement, and it joins with the
lesson's second finding. `toMap` with four independent arguments also
takes a container **supplier** (`TreeMap::new`); once a supplier gets
given, the result container's type is no longer ambiguous, it is
exactly the requested class, and this is a precise outcome confirmed
with `instanceof` in the run. That is, the ambiguity about the result
container's type is not permanent — once the caller gives the
supplier themselves, a promise left to the implementation passes back
into the caller's hands, and the observation becomes as certain as an
interface guarantee.

`toMap`'s three separate forms gather these three measurements into a
single pattern — each extra argument hands one of the collector's four
parts back to the caller:

| Argument count | Parts given | Behavior on duplicate key |
|---|---|---|
| 2 (key, value) | accumulator is the library's default | `IllegalStateException` |
| 3 (+ combiner) | accumulator with the caller's own rule | colliding values merge |
| 4 (+ container supplier) | supplier is the caller's too | merges, and the container's type is fixed too |

The two-argument form leaves the supplier and the combiner to the
library's own default; there is no default combiner, so the only thing
the collector can do at the moment of collision is **stop**. The
three- and four-argument forms hand these two parts back to the
caller in order — the collector's "four parts" structure here is not
an abstract description, it is a fact visible directly in the method
signature. The accumulator is always implicitly fixed: it gets built
from the key and value functions, it never gets written separately.
The finisher is also the same across all four forms — since the
container itself is already the result, no extra transformation is
needed. What changes is only the supplier and the combiner, and these
two, exactly, are what can get left to the caller, because both
correspond to the "which container" question; the "which key, which
value" question is already answered by the first two arguments.

#### Summary

- A collector gets built from four parts: supplier (builds the empty
  container), accumulator (works every item into the container),
  combiner (merges two partial containers), finisher (turns the
  container into the final result).
- `Collectors.toList()`'s returned list being mutable is an
  implementation detail, not guaranteed in the documentation;
  `toUnmodifiableList()` explicitly guarantees immutability and its
  violation falls with `UnsupportedOperationException`.
- `groupingBy`, once it encounters a duplicate key, accumulates the
  values in a list, no item gets lost; two-argument `toMap` throws
  `IllegalStateException` in the same situation.
- Once a merge function gets given, `toMap` merges the colliding
  values; the exception disappears.
- Boundary measurement: once the caller gives an explicit container
  supplier (like `TreeMap::new`), the ambiguity about the result
  container's type closes — a promise left to the implementation can
  get reclaimed by the caller.
- `toMap`'s four forms show these four parts directly in the method
  signature: the two-argument form leaves the supplier and combiner to
  the library, the four-argument form hands both to the caller.

#### Next Step

This lesson measured which promises the container accumulating at the
end of a stream chain gets built with: four parts, the result
container's type left unstated in the documentation, the distinction
between exception and merging on a duplicate key, and how that
distinction can get closed with an explicit container supplier.
Collecting always produces a value — collecting into an empty list
gives an empty list, counting an empty stream gives zero. But once a
single item gets **sought**, what happens if that item does not exist
in the source at all? Some methods of the standard library return null
in this case, some throw an exception. The next lesson takes up a type
where absence gets expressed **as a type**: once absence moves into a
type, does the caller's obligation genuinely move to compile time, or
does the check still stay at runtime?

### `Optional`

Source: https://academia.sh/en/courses/java-standard-library/optional

Expressing absence as a type does not move the caller's obligation to compile time: all four unchecked calls — both in the null-returning signature and the Optional-returning signature — compile unchecked, and two fall at runtime. The gain is visibility in the signature; this gain disappears in a field and a parameter, and of() falls immediately on null while ofNullable() does not.

The previous lesson measured which promises the container
accumulating at the end of a stream chain gets built with. Collecting
always produces a value — but once a single item gets sought, what
happens if that item does not exist in the source at all? Some methods
of the standard library return null in this case, some return a
container called `Optional<T>` that expresses absence **as a type**.
`Optional` itself is not an interface, it is a single class; this
course's "who gives the guarantee" question gets asked here in a third
form: does carrying absence into the type genuinely eliminate the
caller's "check first, then read" obligation, or does it only make it
**visible**? Once absence moves into the type, does the caller's
obligation genuinely move to compile time, or does the check still
stay at runtime?

#### Same Work, Two Signatures: The Check Still Stays at Runtime

- **FJ27** — The same lookup work gets written with two signatures:
  one a `String` that can return null, the other an `Optional<String>`.
  Both get called with an existing and a missing input, with both the
  unchecked and the safe idiom.

```java
// Same.java - the same work, a null-returning signature versus an Optional-returning one: the check left to the caller
import java.util.*;

public class Same {
    static final Map<String, String> COLORS = Map.of("pear", "green");

    static String findColor(String fruit) { return COLORS.get(fruit); }
    static Optional<String> findColorOptional(String fruit) { return Optional.ofNullable(COLORS.get(fruit)); }

    public static void main(String[] args) {
        int checkedCalls = 0, uncheckedCalls = 0;

        try {
            int len1 = findColor("pear").length();
            uncheckedCalls++;
            System.out.println("null-returning signature, existing input : " + len1 + " (compiled unchecked)");
        } catch (NullPointerException e) { System.out.println("unexpected exception"); }

        try {
            int len2 = findColor("apple").length();
            System.out.println("null-returning signature, missing input  : " + len2);
        } catch (NullPointerException e) {
            uncheckedCalls++;
            System.out.println("null-returning signature, missing input  -> exception: "
                    + e.getClass().getSimpleName() + " (compiled unchecked, fell at runtime)");
        }

        try {
            int len3 = findColorOptional("pear").get().length();
            uncheckedCalls++;
            System.out.println("Optional-returning signature, existing input : " + len3 + " (compiled unchecked)");
        } catch (NoSuchElementException e) { System.out.println("unexpected exception"); }

        try {
            int len4 = findColorOptional("apple").get().length();
            System.out.println("Optional-returning signature, missing input  : " + len4);
        } catch (NoSuchElementException e) {
            uncheckedCalls++;
            System.out.println("Optional-returning signature, missing input  -> exception: "
                    + e.getClass().getSimpleName() + " (compiled unchecked, fell at runtime)");
        }

        System.out.println("safe idiom, null-returning signature   : "
                + (findColor("apple") != null ? findColor("apple") : "unknown"));
        checkedCalls++;
        System.out.println("safe idiom, Optional-returning signature: " + findColorOptional("apple").orElse("unknown"));
        checkedCalls++;

        System.out.println();
        System.out.println("calls that compiled unchecked (all four of four): " + uncheckedCalls);
        System.out.println("checked calls (written with the safe idiom)     : " + checkedCalls);
    }
}
```

```
null-returning signature, existing input : 5 (compiled unchecked)
null-returning signature, missing input  -> exception: NullPointerException (compiled unchecked, fell at runtime)
Optional-returning signature, existing input : 5 (compiled unchecked)
Optional-returning signature, missing input  -> exception: NoSuchElementException (compiled unchecked, fell at runtime)
safe idiom, null-returning signature   : unknown
safe idiom, Optional-returning signature: unknown

calls that compiled unchecked (all four of four): 4
checked calls (written with the safe idiom)     : 2
```

All four unchecked calls compile — whether the signature is `String`
or `Optional<String>`, the compiler raises no objection to any of
them. Called with a missing input, both fail at runtime, only the
exception's name changes: `NoSuchElementException` instead of
`NullPointerException`. The measured number is plain here: the count
of calls that can pass unchecked, left to the caller by two signatures
doing the same work, is **equal**. The `Optional` return type does not
drop this count to zero; when the check does not get written, it
still falls at runtime.

The gain is somewhere else: in the **signature itself**. Someone
reading the signature `String findColor(String)` cannot know the
returned value might be null — they only learn this from the
documentation or by reading the source. The signature
`Optional<String> findColorOptional(String)`, though, states the
possibility of absence **directly**; the caller has to look at
neither the documentation nor the source to see this. The check
itself is still optional, but **whether** a check is needed is now
written in the type.

This distinction can be compared to the checked exception built in the
Object-Oriented Java course: there, the exception a method could throw
was written in the signature, and the caller had to handle it —
otherwise compilation stopped; not catching it was an option too, but
the option was at least visible. The `Optional` return type provides
similar visibility, but has no mandatory side: while code that handles
a checked exception nowhere at all does not compile, code that calls
`get()` on an `Optional` with no check at all compiles without a
hitch, as seen in the fourth line. Both mechanisms carry absence or
the error into the signature; only one makes it mandatory, the other
only makes it visible.

#### Chaining Does Not Eliminate the Check, It Defers It

`Optional` is not just a container; it offers a chaining similar to a
stream chain, with methods like `map`, `orElse`, `orElseGet`. This
chaining does not turn a null value into a check that never gets
written, but it at least eliminates an `if (x != null)` block repeated
at every step.

- **FJ28** — `findColorOptional` gets called with the same two inputs
  (`"pear"`, `"apple"`), and the result gets uppercased with `map` and
  bound to a default string with `orElse`; no explicit check gets
  written at any intermediate step.

```java
// Chain.java - how Optional chaining removes the intermediate check
import java.util.*;

public class Chain {
    static final Map<String, String> COLORS = Map.of("pear", "green");
    static Optional<String> findColorOptional(String fruit) { return Optional.ofNullable(COLORS.get(fruit)); }

    public static void main(String[] args) {
        String upperExisting = findColorOptional("pear").map(s -> s.toUpperCase(Locale.ROOT)).orElse("NONE");
        String upperMissing = findColorOptional("apple").map(s -> s.toUpperCase(Locale.ROOT)).orElse("NONE");
        System.out.println("existing input, chain result   : " + upperExisting);
        System.out.println("missing input, chain result    : " + upperMissing);
    }
}
```

```
existing input, chain result   : GREEN
missing input, chain result    : NONE
```

The `map(s -> s.toUpperCase(Locale.ROOT))` call never runs if the
container is empty — the "interface guarantee" measured in the
Predicate and Function lessons holds here too: `map`'s own promise is
never calling the function on an empty container. This is why, once
`findColorOptional("apple")` returns an empty container,
`s.toUpperCase(...)` never runs and no exception falls; the chain
drops straight to `orElse("NONE")`. The gain here is real: had we
written the same work with null, a separate `if` would be needed at
every step.

But this chaining has a trap of its own. `orElse` and `orElseGet` look
like they do the same work — both say "give this if it is missing" —
but **when** they compute their argument differs.

- **FJ29** — The same expensive default-value producer gets called on
  both a full and an empty container, with both `orElse` and
  `orElseGet`; how many times the producer runs gets counted.

```java
// OrElse.java - orElse always computes the expensive default, orElseGet only when needed
import java.util.*;

public class OrElse {
    static int callCount = 0;

    static String expensiveDefault() {
        callCount++;
        return "default";
    }

    public static void main(String[] args) {
        Optional<String> full = Optional.of("pear");

        callCount = 0;
        full.orElse(expensiveDefault());
        System.out.println("orElse() on a full container, default called how many times   : " + callCount);

        callCount = 0;
        full.orElseGet(OrElse::expensiveDefault);
        System.out.println("orElseGet() on a full container, default called how many times: " + callCount);

        Optional<String> empty = Optional.empty();
        callCount = 0;
        empty.orElse(expensiveDefault());
        System.out.println("orElse() on an empty container, default called how many times : " + callCount);

        callCount = 0;
        empty.orElseGet(OrElse::expensiveDefault);
        System.out.println("orElseGet() on an empty container, default called how many times: " + callCount);
    }
}
```

```
orElse() on a full container, default called how many times   : 1
orElseGet() on a full container, default called how many times: 0
orElse() on an empty container, default called how many times : 1
orElseGet() on an empty container, default called how many times: 1
```

`orElse` gets called once in **both** of the two rows where its
container is full — even though the container is full. The reason is
in the syntax: when `orElse(expensiveDefault())` gets written, the
`expensiveDefault()` call, as the argument of a Java method call, gets
evaluated **before** `orElse` itself runs — the producer runs even
when the container is full, `orElse` just does not use the result.
`orElseGet(OrElse::expensiveDefault)`, though, takes a producer
**reference** and calls it only when the container is empty; on a
full container, the producer never runs. This repeats, here, the
laziness measurement from the previous topic: **when** an operation
runs gets understood not by looking at the name of the method
wrapping it, but by **running** it.

#### The Gain Disappears in a Field and a Parameter

The visibility `Optional` earns only works once it gets used as a
**return type**. Once it gets used as a field or parameter type, the
same gain disappears.

- **FJ30** — A class carrying a field of type `Optional<String>` gets
  built, and that field gets assigned null directly — the compiler
  allows this, because `Optional` is an ordinary reference-typed class
  too.

```java
// Field.java - does the gain disappear when Optional gets used in a field
import java.util.*;

public class Field {
    static class Entry {
        Optional<String> label;
    }

    public static void main(String[] args) {
        Entry e = new Entry();
        e.label = null;
        System.out.println("can the field itself be null       : " + (e.label == null));
        try {
            boolean present = e.label.isPresent();
            System.out.println("isPresent() on the field : " + present);
        } catch (NullPointerException ex) {
            System.out.println("isPresent() on the field -> exception: " + ex.getClass().getSimpleName());
        }
    }
}
```

```
can the field itself be null       : true
isPresent() on the field -> exception: NullPointerException
```

Assigning `null` directly to the `Optional<String> label` field
**compiles**. `Optional` is an ordinary class carrying a type
parameter; it is itself a reference, and like every reference, it can
be null. Calling `isPresent()` while the field is null this time
raises `NullPointerException` — exactly the same exception `Optional`
exists to prevent, just **one layer outside**.

This is the lesson's boundary measurement: `Optional`'s gain only
holds in the narrow context of getting used as a method's **return
type**. Used in a field or a parameter, a **doubled** possibility of
absence gets born — the field itself can be null, and the value the
field carries can be null — and the visibility earned turns into a
complexity that takes back more than it earned. The standard
library's own documentation counts this use as not recommended; what
gets measured here is the **reasoning** behind that recommendation.

The reason becomes clear once it joins with the first section's
measurement: `Optional`'s only protection is a **reminder** sitting in
the signature, not a rule enforced at runtime. Used as a return type,
this reminder shows up again at every call site — everyone calling
the method sees the returned value is `Optional` and decides. Used as
a field, though, the reminder shows up only once, in the class's
**definition**; code using the class does not re-read that reminder
every time it accesses the field, because the access looks like an
ordinary field read, and `Optional` itself becomes invisible.
Visibility's gain ends exactly where visibility disappears.

#### of Falls Immediately, ofNullable Does Not

`Optional` has two main ways to build a container: `Optional.of(value)`
and `Optional.ofNullable(value)`. Both return a container of the same
type, but they come apart once called with a null value.

- **FJ31** — `Optional.of(null)` and `Optional.ofNullable(null)` get
  tried separately; `get()` gets called on the second one's result to
  measure when the check falls.

```java
// OfOfNullable.java - of falls immediately, ofNullable does not
import java.util.*;

public class OfOfNullable {
    public static void main(String[] args) {
        try {
            Optional<String> direct = Optional.of(null);
            System.out.println("building of(null): fell through");
        } catch (NullPointerException e) {
            System.out.println("building of(null) -> exception: " + e.getClass().getSimpleName());
        }

        Optional<String> nullable = Optional.ofNullable(null);
        System.out.println("building ofNullable(null): did not fall, result=" + nullable);
        try {
            nullable.get();
            System.out.println("ofNullable(null).get(): succeeded");
        } catch (NoSuchElementException e) {
            System.out.println("ofNullable(null).get() -> exception: " + e.getClass().getSimpleName());
        }
    }
}
```

```
building of(null) -> exception: NullPointerException
building ofNullable(null): did not fall, result=Optional.empty
ofNullable(null).get() -> exception: NoSuchElementException
```

`Optional.of(null)` falls **at the moment of construction**: an `of`
call means "I know this value is not null," and once called with a
null value, this claim gets falsified immediately.
`Optional.ofNullable(null)`, though, does not fall — it already
accepts the value might be null, and returns an empty container
(`Optional.empty()`). The break does not happen there, it happens once
the container's **content gets read**: the `get()` call falls with
`NoSuchElementException`. Both methods can end up at the same point
eventually — calling `get()` on an empty container — but `of` catches
this immediately, at the construction line, while `ofNullable` carries
this risk along with the container itself, and defers the check to the
line where the container gets used.

This deferral is not a defect, it is exactly `Optional`'s job: at the
point `ofNullable` gets called, no one may yet know how the container
will get used — maybe it will go straight into a `map` chain, maybe
it will get assigned to a field, maybe it will never get read at all.
`of` assumes the opposite: it claims, right here, at exactly this
line, that the value is full, and does not wait to confirm this claim.
The difference between the two gives a small answer to the
caller's-obligation question measured at the start of the lesson: the
obligation never disappears, only **when** it falls changes with the
method the caller chooses. Which one to choose is the caller's
decision: if certain the input cannot be null, `of` builds an
early-warning system; if already knowing the input might be null,
`ofNullable` represents this not as an exception, but as an
**ordinary case**.

#### Summary

- A null-returning signature and an `Optional`-returning signature are
  equal in the count of calls left unchecked: all four unchecked
  calls compile, two fall at runtime. `Optional` does not reduce
  this count.
- What gets gained is the signature's visibility: the `Optional<T>`
  return type shows the possibility of absence directly in the
  signature, without looking at the documentation.
- `Optional` supports chaining: `map` never runs on an empty
  container, no separate null check needs writing at intermediate
  steps. But `orElse` always computes its argument even if the
  container is full; `orElseGet` only runs when the container is
  empty — though the two look like they do the same work, their costs
  differ.
- Boundary measurement: the gain disappears once `Optional` gets used
  as a field or a parameter — the field itself can be null, and this
  carries the exception meant to be prevented one layer outward.
- `Optional.of(null)` falls with `NullPointerException` at the moment
  of construction; `Optional.ofNullable(null)` does not fall, it
  returns an empty container and the check gets deferred to the
  `get()` call. The choice is the caller's.

#### Next Step

Across this topic, what a single type — the functional interface, the
lambda, the stream chain, the collector, `Optional` — promises the
library got measured, and every time, the promise was exactly as
narrow as what the compiler checks: single abstract method gets held,
local capture gets frozen, order mostly comes from the source, the
container's type is mostly ambiguous. `Optional` is the last example
of that same narrow promise — absence got stated as a type, the
obligation got carried into the signature, but the check still stayed
at runtime. The next topic asks the same question no longer over the
language's own types, but at the boundary built with the outside
world: it measures what a byte, a file, a clock, and text promise the
standard library.

## Input/Output and Time

Where byte and character streams diverge and the decision encoding leaves to the caller, the promises the file system API gives in path and directory operations, object serialization bypassing the constructor mechanism, what date-time types carry and do not carry, and the behavior of pattern compilation and matching.

### Stream-Based Input/Output

Source: https://academia.sh/en/courses/java-standard-library/stream-based-io

It is measured that the byte stream promises nothing for write(int) beyond the low eight bits, that the character stream leans on an encoding, and that buffering changes only the call count reaching the stream below it, not the promise. The bounding measurement shows an unflushed buffer leaves data incomplete.

The previous topic's last lesson carried absence into a type: when `Optional` was empty, a
read still fell at runtime — the check did not disappear, only its location changed. This
topic asks the same question of the **outside world**. When reading a byte array or a piece of
text, what promise does the library give you, and can you tell, from the call's spelling,
who is carrying that promise — the interface, the class you chose, or you?

This lesson compares two separate families: the byte stream and the character stream. Both
are `java.io` input/output streams; Java's `Stream` data-stream API is a separate API and will
always be referred to with a qualifier throughout this topic. The question is always asked the
same way: if a behavior shows up in two separate implementations, it is the interface's
promise; if it shows up in only one, it belongs to the chosen class; if it never shows up at
all, yet the code still works, it is a rule the caller is following.

Five sections proceed in order. The first two measure what is guaranteed at the byte level —
in writing and in reading. The third shows that buffering never breaks these two promises, it
only changes the call count. The fourth measures the decision the character stream's one
addition over the byte stream — the encoding — places on the caller. The fifth measures a rule
no class gives at all, one only the caller must follow: flushing a buffer.

#### The One Promise the Byte Stream Gives

- **IO1** — `OutputStream.write(int)` takes an integer but writes only its low eight bits; the
  remaining twenty-four are discarded. This measurement checks whether two separate
  implementations write the same value and produce the same low eight bits.
- **IO2** — The written byte, when read back, is converted to an unsigned integer with
  `& 0xFF`; the signed `byte` value is never compared directly.

```java
// OnePromise.java — the one promise the byte stream's write(int) gives: the low eight bits
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;

public class OnePromise {
    public static void main(String[] args) throws Exception {
        int value = 321;
        int expected = value & 0xFF;
        System.out.printf("%-24s%-10s%s%n", "implementation", "written", "same as expected");

        ByteArrayOutputStream array = new ByteArrayOutputStream();
        array.write(value);
        int arrayResult = array.toByteArray()[0] & 0xFF;
        System.out.printf("%-24s%-10d%s%n", "ByteArrayOutputStream", arrayResult, arrayResult == expected);

        Path file = Path.of("one-promise.bin");
        try (OutputStream fileStream = new FileOutputStream(file.toFile())) {
            fileStream.write(value);
        }
        int fileResult = Files.readAllBytes(file)[0] & 0xFF;
        System.out.printf("%-24s%-10d%s%n", "FileOutputStream", fileResult, fileResult == expected);
        Files.delete(file);
    }
}
```

```
implementation          written   same as expected
ByteArrayOutputStream   65        true
FileOutputStream        65        true
```

The written value is 321; in binary, its low eight bits are `01000001`, that is, 65. An
implementation holding data in memory and one writing to a file, with no shared line of code
between them, produce the same low eight bits. This is a promise the `OutputStream` abstract
class documents — the choice of implementation changes nothing here, because the behavior is
already defined in the superclass itself.

The promise the byte stream gives is exactly this much: to carry an eight-bit unit as is. What
the value means — a character, a number, part of an image — is something the stream never
knows at all. A byte stream gives no promise of **interpretation**; it only carries units.
`OutputStream` is not a Java `interface`, it is an abstract class, but that does not change
the question this measure asks: if a behavior is defined in the supertype, that type is
speaking like an interface — every subclass speaking through it has to carry the promise
exactly as is.

#### The Same Promise at the Reading End

- **IO3** — `InputStream.read()` is a promise too: the byte read comes back as an unsigned
  integer, between 0 and 255; once the input/output stream ends, the value returned is not a
  byte, it is a marker outside the stream: **-1**.
- **IO4** — The measured array carries three bytes, and the third is a value that would come
  out negative if read as a signed `byte` (255). Here, whether `read()`'s conversion to
  unsigned carries this value correctly too is seen.

```java
// Reading.java — the one promise for read(): a value 0-255, then -1 at the end
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

public class Reading {
    static List<Integer> readAll(InputStream in) throws Exception {
        List<Integer> values = new ArrayList<>();
        int b;
        while ((b = in.read()) != -1) values.add(b);
        values.add(in.read());
        return values;
    }

    public static void main(String[] args) throws Exception {
        byte[] data = { 10, (byte) 200, (byte) 255 };

        List<Integer> arrayResult = readAll(new ByteArrayInputStream(data));

        Path file = Path.of("reading.bin");
        Files.write(file, data);
        List<Integer> fileResult;
        try (InputStream fileStream = new FileInputStream(file.toFile())) {
            fileResult = readAll(fileStream);
        }
        Files.delete(file);

        System.out.printf("%-24s%s%n", "implementation", "values read (last two: last byte, end of stream)");
        System.out.printf("%-24s%s%n", "ByteArrayInputStream", arrayResult);
        System.out.printf("%-24s%s%n", "FileInputStream", fileResult);
        System.out.println("are the two lists the same: " + arrayResult.equals(fileResult));
    }
}
```

```
implementation          values read (last two: last byte, end of stream)
ByteArrayInputStream    [10, 200, 255, -1]
FileInputStream         [10, 200, 255, -1]
are the two lists the same: true
```

Of the three bytes, the third one, if read as a signed `byte`, would come out **-1**;
`read()` converts it to an unsigned value and returns **255**. Both the in-memory array and
the file give the same three values, followed by the same marker. This `-1` is not a byte that
was read — no real byte can carry this value, because a byte converted to unsigned is always
between 0 and 255. `-1` is a marker sitting outside the data space, telling that the stream has
**ended**.

Preserving the low eight bits on write, and converting to unsigned plus an end-of-stream marker
on read, together set up the byte stream's entire contract. Both are independent of
implementation, because both are defined in `InputStream` and `OutputStream` themselves; a
subclass cannot change this behavior, it can only choose *how* it provides it — in memory, or
on disk.

#### Buffering Changes the Call Count, Not the Promise

- **IO5** — A wrapper is written that counts every write call reaching the stream below it;
  this wrapper only keeps a counter, it does not change the data.
- **IO6** — The same five pieces are written first directly to this wrapper, then through a
  `BufferedOutputStream` to the same wrapper. Two separate things are compared: the number of
  calls reaching the stream below, and the content that stream collects.

```java
// CountingStream.java — a wrapper counting write calls reaching the stream below it (helper class, no main)
import java.io.IOException;
import java.io.OutputStream;

class CountingStream extends OutputStream {
    private final OutputStream under;
    int callCount = 0;

    CountingStream(OutputStream under) { this.under = under; }

    @Override public void write(int b) throws IOException {
        callCount++;
        under.write(b);
    }

    @Override public void write(byte[] data, int off, int len) throws IOException {
        callCount++;
        under.write(data, off, len);
    }

    @Override public void flush() throws IOException { under.flush(); }
    @Override public void close() throws IOException { under.close(); }
}
```

```java
// Buffering.java — buffering changes the call count reaching the stream below it, not the promise
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.util.List;

public class Buffering {
    static final List<byte[]> PIECES = List.of(
            "data".getBytes(), "-".getBytes(), "entry".getBytes(),
            "-".getBytes(), "01".getBytes());

    public static void main(String[] args) throws Exception {
        ByteArrayOutputStream directUnder = new ByteArrayOutputStream();
        CountingStream directCounter = new CountingStream(directUnder);
        for (byte[] piece : PIECES) directCounter.write(piece, 0, piece.length);
        directCounter.close();

        ByteArrayOutputStream bufferedUnder = new ByteArrayOutputStream();
        CountingStream bufferedCounter = new CountingStream(bufferedUnder);
        try (BufferedOutputStream buffered = new BufferedOutputStream(bufferedCounter)) {
            for (byte[] piece : PIECES) buffered.write(piece, 0, piece.length);
        }

        System.out.printf("%-14s%-18s%s%n", "write form", "calls to under", "content delivered");
        System.out.printf("%-14s%-18d%s%n", "direct", directCounter.callCount,
                directUnder.toString());
        System.out.printf("%-14s%-18d%s%n", "buffered", bufferedCounter.callCount,
                bufferedUnder.toString());
        System.out.println("content same: " + directUnder.toString().equals(bufferedUnder.toString()));
    }
}
```

```
write form    calls to under    content delivered
direct        5                 data-entry-01
buffered      1                 data-entry-01
content same: true
```

In the direct write, the five pieces reach the counting stream as five separate calls: every
`write` lands on the stream below immediately. In the buffered write, the same five pieces
reach the counting stream as **one** call, because `BufferedOutputStream` collects the pieces
in its own memory and calls the stream below only when it empties. Both writes deliver
byte-for-byte identical content.

This measurement separates what buffering changes from what it does not. `OutputStream`'s
promise reads "the bytes you write will eventually reach the stream below"; it does not read
"every `write` call will touch the stream below instantly." Buffering never violates this
second sentence, because that sentence was never part of the promise to begin with. What
changes is only a number the caller cannot see: how many calls reach the stream below.

This is why buffering is a choice, not a guarantee. If the stream below is genuinely expensive
— a file, a network connection — five separate calls can turn into five separate system calls;
once a buffer steps in, the same job comes down to one call. Since the choice belongs to the
caller, `BufferedOutputStream` never flushes on its own: only the caller decides when to fill
the buffer and when to flush it early. This lesson's last section's bounding measurement shows
exactly the case where this decision gets skipped.

#### The Character Stream Leaning on an Encoding

- **IO7** — A piece of text is converted to bytes with a single encoding and written. The same
  byte array is read back with two separate encodings: the one it was written with, and
  another.
- **IO8** — Three things are compared: byte count (stays fixed, because the source is the
  same), character count, and whether the text read equals the text written. Not the text
  itself, only length and equality are printed.

```java
// Encoding.java — the character stream's promise depends on an encoding, the byte stream's does not
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class Encoding {
    public static void main(String[] args) throws Exception {
        String text = "ağırlık";
        Path file = Path.of("encoding.bin");

        byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
        Files.write(file, bytes);

        String sameEncoding = new String(Files.readAllBytes(file), StandardCharsets.UTF_8);
        String otherEncoding = new String(Files.readAllBytes(file), StandardCharsets.ISO_8859_1);

        System.out.printf("%-29s%-12s%-16s%s%n", "read encoding", "byte count", "char count", "same as written");
        System.out.printf("%-29s%-12d%-16d%s%n", "UTF_8 (as written)", bytes.length,
                sameEncoding.length(), sameEncoding.equals(text));
        System.out.printf("%-29s%-12d%-16d%s%n", "ISO_8859_1 (other encoding)", bytes.length,
                otherEncoding.length(), otherEncoding.equals(text));

        Files.delete(file);
    }
}
```

```
read encoding                byte count  char count      same as written
UTF_8 (as written)           10          7               true
ISO_8859_1 (other encoding)  10          10              false
```

A seven-letter piece of text produces ten bytes in UTF-8, because two of its letters are shown
with two bytes each in this encoding. The same ten bytes, read back with UTF-8, turn into seven
characters and match what was written exactly. The same ten bytes, read with ISO-8859-1, have
each byte counted as its own character, giving ten characters — not matching the text that was
written.

This measurement really does break a rule of the caller's: that the write encoding and the
read encoding be the same. The break's consequence is neither an exception nor a warning; it is
a **silent** result. The program runs without error, produces a string, and that string is
wrong. It was established in the previous section that the byte stream gives no promise of
interpretation at all; the character stream fills exactly that gap — but the encoding filling
it has to be given by **you**. When no encoding is given, the runtime falls back to a
default; since Java 18 that default is UTF-8, independent of the operating system and the
locale — this is exactly why this lesson gives the
encoding explicitly on every read and write: the table above is exactly why, when the two sides
use different encodings, the error never stops anywhere, only the number changes.

Where this burden is carried is itself a measurable choice. `OutputStreamWriter` and
`InputStreamReader` — the two classes converting a byte stream into a character stream — ask
for a `Charset` parameter in their constructors; `Files.write` and `Files.readAllBytes`, in
turn, are completely unaware of encoding, because both carry only bytes and leave the encoding
decision to the caller, as above, at `getBytes` and the `String` constructor. Whichever layer
it is given at, the decision is not part of the promise; every read and every write has to
carry its own encoding explicitly.

#### The Unflushed Buffer

- **IO9** — Right after a `BufferedOutputStream` is written to, without calling `flush`, the
  content of the stream below it is looked at directly. In the second measurement, the same
  steps are completed with `flush`.

```java
// Flushing.java — an unflushed buffer does not carry the data; flushing is the caller's decision
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;

public class Flushing {
    public static void main(String[] args) throws Exception {
        ByteArrayOutputStream underNoFlush = new ByteArrayOutputStream();
        BufferedOutputStream bufferedNoFlush = new BufferedOutputStream(underNoFlush);
        bufferedNoFlush.write("will not be delivered".getBytes());

        ByteArrayOutputStream underFlushed = new ByteArrayOutputStream();
        BufferedOutputStream bufferedFlushed = new BufferedOutputStream(underFlushed);
        bufferedFlushed.write("will be delivered".getBytes());
        bufferedFlushed.flush();

        System.out.printf("%-26s%-13s%s%n", "measurement", "under length", "data in under stream");
        System.out.printf("%-26s%-13d%s%n", "before flush called",
                underNoFlush.size(), underNoFlush.size() > 0);
        System.out.printf("%-26s%-13d%s%n", "after flush called",
                underFlushed.size(), underFlushed.size() > 0);
    }
}
```

```
measurement               under length data in under stream
before flush called       0            false
after flush called        17           true
```

Without `flush` called, the stream below has zero length: the twenty-one-byte text is still
waiting in `BufferedOutputStream`'s own memory, and the stream below has never been touched.
Once `flush` is called, the same mechanism delivers its seventeen bytes in full. The difference
is not in the code's correctness, it is in **whether a call was made** — and making that call
is entirely the caller's decision.

Try-with-resources (Object-Oriented Java, `java-generics/04`) automates this call: if a
`BufferedOutputStream` is opened inside `try (...)`, `close` runs when the block exits, and
`close` calls `flush` internally. But for this automation to kick in, the buffered stream has
to be **declared as a resource**; in the first measurement above, `bufferedNoFlush` never
entered any `try (...)` block, so closing never ran and the data was lost. Try-with-resources
prevents the loss, but writing the line that prevents it is still left to the caller.

Some classes take this decision onto themselves: one of `PrintWriter`'s constructors asks, with
a `boolean` parameter, whether it should auto-flush at the end of every line.
`BufferedOutputStream` has no such option — it always stays manual. Even this difference
confirms the same rule: auto-flushing is a behavior a class *chooses* to have, not an interface
promise **every** input/output stream class follows. Which class you choose also decides
whether you are leaving the flushing decision to yourself, or handing it to the class.

#### Summary

- The only promise the byte stream gives for `write(int)` is to carry the low eight bits; this
  promise comes out the same across different implementations, because it is defined in the
  superclass itself.
- Buffering changes the call count reaching the stream below, not the content delivered: five
  calls come down to one, but the result stays byte-for-byte identical.
- The character stream leans on an encoding; the byte stream gives no promise of
  interpretation. When no encoding is given, the runtime's default takes over; since Java 18
  that default is UTF-8.
- When the write and read encodings come apart, the error never stops anywhere: a
  seven-character piece of text silently turns into ten characters.
- Bounding measurement: an unflushed buffer leaves data incomplete. Try-with-resources
  prevents this loss, but declaring the buffered stream as a resource is the caller's decision.

#### Next Step

Everything measured in this lesson concerned content passing through a stream: which byte was
written, which character was read. But there is one more question before a stream is even
opened: does the thing at the stream's end really exist, and what does the path leading to it
say? The next lesson looks at the file system API — it measures that a path is itself a parsed
value, which operations on that value answer without ever touching the file system, and which
ones really reach down to disk.

### File System API

Source: https://academia.sh/en/courses/java-standard-library/file-system-api

That a path is a parsed value, and that normalization and resolution answer without ever touching the file system, is shown by running it on two separate providers. It is measured that directory traversal order is unspecified, and that existence-checking and opening are two separate operations.

The previous lesson concerned content passing through an input/output stream. But there is
one more question before a stream is even opened: does the thing at the stream's end really
exist, and what does the path leading to it say? This lesson looks at the file system API —
it measures that a path is itself a parsed value, which operations on that value answer
without ever touching the file system, and which ones really reach down to disk.

A `Path` is not a string. The text `"a/../b"` looks like something held inside a string, but
the methods called on it follow the rules not of a text-processing library, but of a **file
system provider**. This lesson's question is asked the same way as the course's: which
behavior is independent of provider choice — that is, the interface's promise; which cannot
be answered without asking the provider's own storage — that is, a storage query; and which
guarantee no method gives at all, one obtained only by making calls in the right order.

Four sections proceed in order. The first two separate two kinds of methods called on the same
path object: those that never touch storage, and those that really ask storage. The third
measures where a directory's listing order comes from, and what it does not guarantee. The
fourth shows the cost of splitting the promise a single method gives into two separate calls.
All four land on the same principle: it is not a method's name, it is when and which resource
it asks, that decides the promise that method gives.

#### Path Algebra: Never Touching the File System

- **IO10** — The same three operations — normalization, resolution, file-name separation —
  are run on two separate file system providers: the default local provider, and the standard
  library's own zip provider (a provider, part of the standard library, that opens a zip
  archive like a file system).
- **IO11** — What is compared is the **equality** of two results; no path string on its own
  needs interpretation, because the results are already short, human-readable strings. The
  same measurement is repeated for `equals` and `compareTo`, and it is also checked what
  `equals` says when two paths, textually identical, come from different providers.

For this measurement, what is compared is not two separate classes, it is two separate
**providers**: the default local provider, and the standard library's own zip provider, a
`FileSystemProvider` implementation that opens a zip archive like a file system. In the
course's previous lessons, "implementation" meant choosing between a `List` or a `Map`; here,
provider choice plays the same role — which file system you are running behind does not
change the methods you call on the `Path` interface.

```java
// ProviderTrial.java — is path algebra the same across two file system providers
import java.net.URI;
import java.nio.file.*;
import java.util.Map;

public class ProviderTrial {
    public static void main(String[] args) throws Exception {
        Path archive = Path.of("archive.zip");
        Files.deleteIfExists(archive);
        try (FileSystem zip = FileSystems.newFileSystem(
                URI.create("jar:" + archive.toUri()), Map.of("create", "true"))) {

            System.out.printf("%-14s%-20s%-20s%s%n", "operation", "local", "zip", "same");

            String localNorm = Path.of("a/./b/../c").normalize().toString();
            String zipNorm = zip.getPath("a/./b/../c").normalize().toString();
            System.out.printf("%-14s%-20s%-20s%s%n", "normalize", localNorm, zipNorm,
                    localNorm.equals(zipNorm));

            String localResolve = Path.of("root").resolve("sub/file.txt").toString();
            String zipResolve = zip.getPath("root").resolve("sub/file.txt").toString();
            System.out.printf("%-14s%-20s%-20s%s%n", "resolve", localResolve, zipResolve,
                    localResolve.equals(zipResolve));

            String localName = Path.of("dir/file.txt").getFileName().toString();
            String zipName = zip.getPath("dir/file.txt").getFileName().toString();
            System.out.printf("%-14s%-20s%-20s%s%n", "getFileName", localName, zipName,
                    localName.equals(zipName));

            String localRelative = Path.of("a/b/c").relativize(Path.of("a/x")).toString();
            String zipRelative = zip.getPath("a/b/c").relativize(zip.getPath("a/x")).toString();
            System.out.printf("%-14s%-20s%-20s%s%n", "relativize", localRelative, zipRelative,
                    localRelative.equals(zipRelative));

            boolean localEqual = Path.of("a/b").equals(Path.of("a/b"));
            boolean zipEqual = zip.getPath("a/b").equals(zip.getPath("a/b"));
            System.out.printf("%-14s%-20s%-20s%s%n", "equals (own)", localEqual, zipEqual,
                    localEqual == zipEqual);

            boolean localOrder = Path.of("a/b").compareTo(Path.of("a/c")) < 0;
            boolean zipOrder = zip.getPath("a/b").compareTo(zip.getPath("a/c")) < 0;
            System.out.printf("%-14s%-20s%-20s%s%n", "compareTo<0", localOrder, zipOrder,
                    localOrder == zipOrder);

            System.out.println("local a/b equal to zip a/b (same text, different provider): "
                    + Path.of("a/b").equals(zip.getPath("a/b")));
        }
        Files.deleteIfExists(archive);
    }
}
```

```
operation     local               zip                 same
normalize     a/c                 a/c                 true
resolve       root/sub/file.txt   root/sub/file.txt   true
getFileName   file.txt            file.txt            true
relativize    ../../x             ../../x             true
equals (own)  true                true                true
compareTo<0   true                true                true
local a/b equal to zip a/b (same text, different provider): false
```

All six of the six operations give byte-for-byte identical results on both providers.
`normalize` collapses `.` and `..` segments by the same rule, `resolve` appends one path onto
another the same way, `relativize` computes the relative path between two paths the same way,
and `equals` and `compareTo` also compare the segment list. **None** of these six operations,
while running, looks at the provider's storage — the zip archive is still empty, the local
directory has nothing under these names either, and yet all six produce a result. Because the
result is provider-independent, this behavior is `Path`'s own interface promise: path algebra
is a pure computation working on a list of segments, and never asks about the storage's
content.

The last line shows this promise's boundary. Two paths, textually identical — one from the
local provider, one from the zip provider — are **not equal** when compared with `equals`.
`Path.equals` looks not only at the segments, but also at which provider they belong to; this,
too, is a provider-independent rule, because both providers enforce this restriction the same
way. The promise path algebra gives is not "the same segments produce the same result," it is
"the same segments on the same provider produce the same result."

This purity has a measurable consequence: a rule built on top of path algebra — validating an
extension, blocking an escape into a parent directory, computing a relative path — can be
tested without creating a single file, without setting up a single directory. The measurement
above shows this itself: the zip archive was opened empty, the local directory had nothing
under the relevant names, and all six of the six comparisons completed without error. Had this
been an operation touching storage, the same measurement would first have required a setup.

#### The Same Question's Answer That Touches Storage

- **IO12** — On the same two providers, `Files.exists` is called this time. A record is
  written first to the path on the zip provider, while the same-named file on the local
  provider is never created at all.

```java
// TouchingStorage.java — operations touching storage ask the provider, path algebra never does
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.Map;

public class TouchingStorage {
    public static void main(String[] args) throws Exception {
        Path archive = Path.of("archive.zip");
        Files.deleteIfExists(archive);
        try (FileSystem zip = FileSystems.newFileSystem(
                URI.create("jar:" + archive.toUri()), Map.of("create", "true"))) {

            Path zipFile = zip.getPath("record.txt");
            Files.writeString(zipFile, "data", StandardCharsets.UTF_8);

            Path localFile = Path.of("record.txt");
            Files.deleteIfExists(localFile);

            System.out.printf("%-14s%s%n", "provider", "Files.exists result");
            System.out.printf("%-14s%s%n", "local", Files.exists(localFile));
            System.out.printf("%-14s%s%n", "zip", Files.exists(zipFile));
        }
        Files.deleteIfExists(archive);
    }
}
```

```
provider      Files.exists result
local         false
zip           true
```

The same name gives two separate answers on two separate providers — and this is not a
defect, it is proof of what `Files.exists` really does. Unlike the six operations in the
previous section, `exists` really asks the provider's own storage; the answer can be "no" or
"yes," and that answer depends on what is in storage at that moment. `Files.newInputStream`,
`Files.size`, `Files.readAllBytes`, `Files.isDirectory` belong to the same family: all of them
go to the provider's storage the moment they are called. A shortcut for telling whether a
method called on a path object touches storage: if the result can be produced even while
storage is empty, it is algebra; if it requires storage to hold something, it is a storage
query.

This distinction has a practical consequence: a path object can safely be built, stored, and
combined with other paths even while the file it points to does not exist at all. Naming an
output directory in advance with `resolve`, before it is ever written, blows up nothing at
all, because algebra never asks storage. A problem can only surface once a call touching
storage arrives — `Files.createDirectories` or `Files.newOutputStream`, say — and at that
point, exactly where to look for the error is clear, because the methods touching storage form
a bounded, named set.

Every answer that touches storage also has a time dimension: `exists` gives an answer that is
correct at the **moment** it is called; it gives no promise that the answer stays correct
after the call ends. Path algebra, in this respect, carries a stronger guarantee — once
`normalize` is computed, it never becomes invalid again, because it never looked at storage at
all. A storage query's result, in turn, is a snapshot, and this lesson's last section measures
exactly how short-lived that snapshot is.

#### Directory Traversal Order Is Unspecified

- **IO13** — Four files are created in a directory in a given order. The traversal order
  obtained with `Files.list` is compared against both the creation order and alphabetical
  order.
- **IO14** — The traversal order itself is not printed; only the two comparisons' results and
  whether two separate traversal methods agree with each other are printed. What `Files.list`
  returns is also a resource — it holds a directory handle behind it — and is opened with
  try-with-resources (Object-Oriented Java, `java-generics/04`).

```java
// TraversalOrder.java — directory traversal order is unspecified, ordering is up to the caller
import java.nio.file.*;
import java.util.*;
import java.util.stream.Collectors;

public class TraversalOrder {
    public static void main(String[] args) throws Exception {
        Path dir = Path.of("traversal");
        Files.createDirectories(dir);
        List<String> creationOrder = List.of("many.txt", "few.txt", "mid.txt", "one.txt", "top.txt", "low.txt");
        for (String name : creationOrder) Files.createFile(dir.resolve(name));

        List<String> traversalList;
        try (var stream = Files.list(dir)) {
            traversalList = stream.map(p -> p.getFileName().toString()).collect(Collectors.toList());
        }
        List<String> traversalDirStream = new ArrayList<>();
        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dir)) {
            for (Path p : dirStream) traversalDirStream.add(p.getFileName().toString());
        }
        List<String> alphabetical = traversalList.stream().sorted().collect(Collectors.toList());

        System.out.println("traversal order same as creation order: "
                + traversalList.equals(creationOrder));
        System.out.println("traversal order same as alphabetical order: "
                + traversalList.equals(alphabetical));
        System.out.println("does Files.list give the same order as the directory stream: "
                + traversalList.equals(traversalDirStream));

        for (String name : creationOrder) Files.deleteIfExists(dir.resolve(name));
        Files.deleteIfExists(dir);
    }
}
```

```
traversal order same as creation order: false
traversal order same as alphabetical order: false
does Files.list give the same order as the directory stream: true
```

Traversal order matches neither creation order nor alphabetical order; the provider lists its
own storage according to its own internal layout, and this layout is not a documented promise
of `Files.list`. In contrast, `Files.list` (a method returning a stream) and
`Files.newDirectoryStream` (an iterator interface, in the input/output-stream sense,
returning a directory stream) give the **same** order on the same traversal — because both
wrap the same underlying listing call from the same provider. The two APIs agreeing with each
other does not mean traversal has a **documented** order; it only shows they rest on the same
provider call. A caller wanting a sorted result has to sort the list itself; the API gives no
such promise anywhere. The order also depends on the file system: on some file systems,
macOS's APFS for example, traversal can preserve creation order for a small set of files; the
`false` on the creation-order line above is not a guarantee but the result of this run's file
system.

The source of the uncertainty is the provider's own storage structure. A directory entry is
mostly held not by name, but by the file system's own internal indexing scheme, and listing
walks that internal layout as is. `Files.list`'s documentation never commits to this layout at
all; the only thing it commits to is that every entry appears in the list **exactly once**.
This has the same shape as the previous lesson's buffering measurement: silently assuming a
promise the API never gave builds a dependency that goes unnoticed as long as the code runs,
and surfaces only once it is moved elsewhere.

#### Existence-Checking and Opening Are Two Separate Operations

This section turns the previous section's last sentence into a numeric measurement.
`Files.exists` and `Files.newInputStream` are two separate storage queries, and the time
passing between them is not zero; code running in that gap can change storage's state. The
measurement sets this up with a real deletion, but the source does not matter — the same gap
could just as well be filled by another process, another thread, or the user themselves.

- **IO15** — A file is written, its existence is checked with `Files.exists`, and right after,
  the file is deleted — standing in for another code path stepping in. After the check, an
  attempt is made to open the file.
- **IO16** — For comparison, the same open call is also tried directly on a file that never
  existed at all; it is seen that both errors come from the same class.

```java
// TwoSteps.java — checking existence and opening are two separate operations, a gap sits between them
import java.nio.file.*;

public class TwoSteps {
    public static void main(String[] args) throws Exception {
        Path file = Path.of("temp-record.txt");
        Files.writeString(file, "record");

        boolean checkResult = Files.exists(file);
        Files.delete(file);

        String openResult;
        try {
            Files.newInputStream(file).close();
            openResult = "opened";
        } catch (NoSuchFileException e) {
            openResult = "could not open: " + e.getClass().getSimpleName();
        }
        System.out.printf("%-18s%s%n", "check result", checkResult);
        System.out.printf("%-18s%s%n", "open result", openResult);

        String directResult;
        try {
            Files.newInputStream(Path.of("does-not-exist.txt")).close();
            directResult = "opened";
        } catch (NoSuchFileException e) {
            directResult = "could not open: " + e.getClass().getSimpleName();
        }
        System.out.printf("%-18s%s%n", "direct open", directResult);
    }
}
```

```
check result      true
open result       could not open: NoSuchFileException
direct open       could not open: NoSuchFileException
```

Right after the check returns `true`, opening fails, because the file was deleted in between.
This defect does not come from `Files.exists` lying — the check gave the correct answer at the
moment it was called. The defect comes from the check and the open being **two separate
calls**, and from something else being able to change storage in the time passing between
them. The last line confirms this: trying to open a file that never existed at all directly
also drops the same exception, from the same class. The API already offers the promise "open
or give an error" in a **single call**; it is the caller who adds the checking step in front
of it, and that extra step buys no safety, it only widens the gap. The right order is to open
without checking, and catch the open's own exception.

This is not a gap in the library — the interface is already designed to carry both the result
and the error in a single call. The gap only appears once the caller separates the two calls
and puts other code between them; no matter how short it looks, every line sitting between the
two steps widens that gap. This is the plainest form of what the course calls the "caller's
obligation": the library said nothing wrong, the caller used correct information at the wrong
time.

#### Summary

- `Path` is not a string, it is a parsed value; `normalize`, `resolve`, `getFileName`,
  `relativize`, `equals`, and `compareTo` run without ever touching the file system, and this
  behavior is the same on two separate providers.
- Two textually identical paths from different providers are not equal with `equals`; equality
  looks not just at the segments, but at the segments plus the provider.
- Operations like `Files.exists`, `Files.size`, `Files.readAllBytes` really ask the provider's
  storage; the answer depends on storage's real state and can change with the provider.
- Directory traversal order matches neither creation order nor alphabetical order, and the API
  documents this order nowhere; a caller wanting a sorted result has to sort the list itself.
- Two separate traversal methods giving the same order does not mean the order is documented —
  it only shows they rest on the same provider call.
- Bounding measurement: existence-checking and opening are two separate operations. If the
  file is deleted in between, the check passes and the open falls; the API already offers
  "open or give an error" in a single call, and the extra check is a gap the caller added
  themselves.

#### Next Step

This lesson measured whether a file exists, and which bytes it carries — but never asked what
happens once an object itself is turned into bytes. The next lesson looks at serialization: it
measures that when an object is dumped to a byte array and read back, the object's constructor
never runs at all, and that the invariants a class establishes can, for this reason, be broken
by bytes given from outside.

### Serialization

Source: https://academia.sh/en/courses/java-standard-library/serialization

Serialization is measured as never running the constructor, and as a consequence the invariants a constructor sets up can be skipped by a byte stream that arrived from outside. It shows how a readback hook brings the check back, and how a field name together with a version ID turns into a contract.

The previous lesson measured whether a file exists and which bytes it carries — but it never
asked what happens when an object itself is turned into bytes. This lesson looks at
serialization. The Object-Oriented Java course (M08/K02, `java-classes/01`, `02`) built up the
constructor chain and class invariants: an object can come into existence only by passing
through its constructor, and the constructor is where an invariant gets enforced.
Serialization stands **outside** this rule — turning an object into bytes and reading it back
is the standard library's way of producing an object without ever calling a constructor.

The course's question gets asked again here: does an invariant the constructor set up still
hold after a readback? A single class is enough to find the answer, because what is being
compared is not two different implementations but the same class coming into existence
through two different paths — through the constructor, and through serialization.

Three sections proceed in order. The first shows that the constructor is skipped and counts
the cost. The second shows how that cost can be recovered. The third goes beyond the
invariants and measures how the class's own **shape** — its field names and its version ID —
turns into a contract at readback.

This is one of the sources K03 calls "the caller's obligation," but in an unusual form: here
the caller is not the party that breaks the invariant, it is the party **obligated to recover
it**. The library made no promise and broke none; writing the line that fills the gap was
always up to whoever designed the class.

#### The Constructor Never Runs

- **IO17** — The `Account` class enforces two invariants in its constructor: the owner name
  cannot be blank, the balance cannot be negative. Both are checked with a single `if` line
  each, inside the constructor.
- **IO18** — In hand is a byte stream produced without ever calling the constructor: an
  `Account` record with a blank owner and a negative balance. What matters in this lesson is
  not **how** that byte stream was produced but what happens when it is read back. The risk is
  measured by counting how many of the invariants the constructor set up still hold after the
  readback.

**Every** class that implements `Serializable` can be the subject of this measurement; the
interface requires no methods, it is only a marker. `Account` is not a special example picked
for this lesson, it is a single instance of a general behavior the library applies to any
serializable class. The measurement below demonstrates that behavior with a byte stream
already in hand; whether that stream came from a file, a network connection, or an earlier run
of the program itself makes no difference to the measurement — no matter where it came from,
the readback follows the same path.

```java
// ConstructorSkipped.java — serialization never runs the constructor, the invariants it sets up are not preserved
import java.io.*;
import java.util.Base64;

class Account implements Serializable {
    private static final long serialVersionUID = 1L;
    private final String owner;
    private final long balance;

    Account(String owner, long balance) {
        if (owner == null || owner.isBlank()) throw new IllegalArgumentException("empty owner");
        if (balance < 0) throw new IllegalArgumentException("negative balance");
        this.owner = owner;
        this.balance = balance;
    }

    String owner() { return owner; }
    long balance() { return balance; }
}

public class ConstructorSkipped {
    // A byte stream produced without ever running the constructor: blank owner, negative balance.
    static final String BROKEN_RECORD =
            "rO0ABXNyAAdBY2NvdW50AAAAAAAAAAECAAJKAAdiYWxhbmNlTAAFb3duZXJ0ABJMamF2YS9sYW5nL1N0cmluZzt4cP/////////OdAAA";

    static int validInvariants(Account a) {
        int count = 0;
        if (a.owner() != null && !a.owner().isBlank()) count++;
        if (a.balance() >= 0) count++;
        return count;
    }

    public static void main(String[] args) throws Exception {
        int constructorBlocked = 0;
        try {
            new Account("", -50L);
        } catch (IllegalArgumentException e) {
            constructorBlocked = 1;
        }
        System.out.println("does the constructor enforce both invariants: " + (constructorBlocked == 1));

        byte[] bytes = Base64.getDecoder().decode(BROKEN_RECORD);
        Account readBack;
        try (ObjectInputStream i = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
            readBack = (Account) i.readObject();
        }
        System.out.println("does the readback succeed without ever running the constructor: true");
        System.out.printf("of the two invariants the constructor sets up, how many still hold: %d / 2%n",
                validInvariants(readBack));
    }
}
```

```
does the constructor enforce both invariants: true
does the readback succeed without ever running the constructor: true
of the two invariants the constructor sets up, how many still hold: 0 / 2
```

The first line confirms that a broken call going directly through the constructor is
rejected: the check runs there. The second line shows that the same class can read back a
record carrying **the same brokenness** — no exception falls, `readObject` completes without
issue. The third line counts the cost of that: of the two invariants the constructor set up,
**zero** still hold after the readback.

The reason is how `ObjectInputStream` works. The default readback writes the values from the
stream **directly** into the class's fields; in doing so it never calls the class's own
constructor, passes
through no `if` line. Even `final` fields can be written this way, because serialization is
the language's own mechanism and it does not enter through the constructor's door — it builds
the object directly in memory. This is the **only** exception to the rule K02 established, "an
object can come into existence only by passing through its constructor" — and the exception
is not a weakness in the rule, it is the very **definition** of serialization: the readback's
job is not to call a constructor, it is to reconstruct an object exactly as it was.

This path is not entirely closed off either: if `Account` did not implement `Serializable` at
all, the `writeObject` call itself would fall with `NotSerializableException` and the readback
question would never come up. Declaring the interface is the decision that **opens** the class
to this second path — and that decision belongs to whoever designed the class, it is not
something serialization imposes.

K02's encapsulation lesson established that making a field `private` makes it modifiable only
through the class's own methods. Serialization does not tear this wall down, but it **goes
around it**: there is still no `private` setter, no ordinary call can assign directly into the
`balance` field from outside — but a readback is not an ordinary call, it is the virtual
machine's own mechanism and it never trips over the access modifier. Encapsulation restricts
**method calls** coming from outside; serialization is not a method call, it is a
reconstruction that belongs to the language itself.

#### A Readback Hook Brings the Check Back

`ObjectInputStream`'s documentation defines an escape route: if a class defines a method whose
signature is exactly `private void readObject(ObjectInputStream)`, the readback drops the
default field-filling behavior and calls this method instead. The signature is a contract that
must be followed to the letter — if the method becomes `public`, if the parameter type
changes, or if the name is spelled differently, the readback never sees it and silently falls
back to the default behavior.

- **IO19** — A private method named `readObject` is added to `Account`. This method first
  fills the fields as usual with `defaultReadObject`, then repeats the **same two** checks
  that are in the constructor.

```java
// ReadbackHook.java — once a readObject hook is written, both invariants are checked again on readback
import java.io.*;
import java.util.Base64;

class Account implements Serializable {
    private static final long serialVersionUID = 1L;
    private final String owner;
    private final long balance;

    Account(String owner, long balance) {
        if (owner == null || owner.isBlank()) throw new IllegalArgumentException("empty owner");
        if (balance < 0) throw new IllegalArgumentException("negative balance");
        this.owner = owner;
        this.balance = balance;
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        if (owner == null || owner.isBlank()) throw new InvalidObjectException("empty owner");
        if (balance < 0) throw new InvalidObjectException("negative balance: " + balance);
    }
}

public class ReadbackHook {
    static final String BROKEN_RECORD =
            "rO0ABXNyAAdBY2NvdW50AAAAAAAAAAECAAJKAAdiYWxhbmNlTAAFb3duZXJ0ABJMamF2YS9sYW5nL1N0cmluZzt4cP/////////OdAAA";

    public static void main(String[] args) throws Exception {
        byte[] bytes = Base64.getDecoder().decode(BROKEN_RECORD);
        String result;
        try (ObjectInputStream i = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
            i.readObject();
            result = "opened";
        } catch (InvalidObjectException e) {
            result = "rejected: " + e.getMessage();
        }
        System.out.println("same broken record after the hook is written: " + result);
    }
}
```

```
same broken record after the hook is written: rejected: empty owner
```

The **exact same** byte stream that was read in the previous section is rejected this time.
The only thing that changed is a two-line method added to the class: the call to
`defaultReadObject` fills the fields as usual, then the two `if` lines that follow set up the
**exact same** check as the one in the constructor. `ObjectInputStream` calls a method named
`readObject` instead of the default behavior whenever it finds one on a class; if the method
throws an exception, the readback is left unfinished too and the object is never produced.

What this measurement says is that serialization's promise was **not incomplete**. The promise
was always "I reconstruct an object exactly as it was, I fill its fields directly," and there
was never any invariant checking in that promise — because that check was the constructor's
job, not serialization's. What was needed to bring the check back was not a fix from the
library but the class writing its **own** `readObject`; writing this line is still, as always,
up to the caller — that is, whoever designed the class.

The course's third claim is confirmed here too: none of this gap is caught by the compiler.
Both the hookless `Account` and the hooked `Account` compile with the same lack of complaint;
the compiler never checks whether a class has a `readObject`, or whether the one it has
repeats the checks that are in the constructor. `Serializable` is a marker interface — it
carries no abstract method — and so there is nothing that can be enforced at compile time;
every check is left to the optional body of an optional method.

#### A Field Name and a Version ID Are a Contract

- **IO20** — In hand is an old `Account` record whose version ID is `42`; at that time the
  class's field was named `balance`. In the current class the same field has been renamed to
  `amount`, but the version ID is still `42`.
- **IO21** — The same old record is read this time against a class definition whose version ID
  is `43` — measuring what happens if the version ID had changed too.

```java
// FieldNameContract.java — the field name and the serialization version ID turn into a contract
import java.io.*;
import java.util.Base64;

class Account implements Serializable {
    private static final long serialVersionUID = 42L;
    private final String owner;
    private final long amount;
    Account(String owner, long amount) {
        if (amount < 0) throw new IllegalArgumentException("negative amount");
        this.owner = owner;
        this.amount = amount;
    }
    long amount() { return amount; }
    String owner() { return owner; }
}

public class FieldNameContract {
    // In the old version the same class carried a field named "balance", not "amount";
    // the serialization version ID (42L) has not been changed since then.
    static final String OLD_RECORD =
            "rO0ABXNyAAdBY2NvdW50AAAAAAAAACoCAAJKAAdiYWxhbmNlTAAFb3duZXJ0ABJMamF2YS9sYW5nL1N0cmluZzt4cAAAAAAAAABkdAADYW15";

    public static void main(String[] args) throws Exception {
        byte[] bytes = Base64.getDecoder().decode(OLD_RECORD);
        Account h;
        try (ObjectInputStream i = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
            h = (Account) i.readObject();
        }
        System.out.println("passed (no exception): true");
        System.out.println("owner field (name unchanged): " + h.owner());
        System.out.println("amount field (used to be balance): " + h.amount());
    }
}
```

```
passed (no exception): true
owner field (name unchanged): amy
amount field (used to be balance): 0
```

The readback raises no objection at all. The `owner` field fills without issue because its
name did not change: `amy`. But the `amount` field **never sees** the value the record in the
stream carried under the name `balance` — it is filled with the `long` type's default value,
zero. `ObjectInputStream` maps every field in the stream by its name; if the stream has no
field named `amount`, the class's `amount` is treated as never touched at all. Because the
version ID stayed the same, no warning appears either: as far as the library is concerned,
this is a valid readback.

```java
// VersionIdMismatch.java — the same byte stream is rejected once the version ID changes
import java.io.*;
import java.util.Base64;

class Account implements Serializable {
    private static final long serialVersionUID = 43L;
    private final String owner;
    private final long amount;
    Account(String owner, long amount) {
        if (amount < 0) throw new IllegalArgumentException("negative amount");
        this.owner = owner;
        this.amount = amount;
    }
}

public class VersionIdMismatch {
    static final String OLD_RECORD =
            "rO0ABXNyAAdBY2NvdW50AAAAAAAAACoCAAJKAAdiYWxhbmNlTAAFb3duZXJ0ABJMamF2YS9sYW5nL1N0cmluZzt4cAAAAAAAAABkdAADYW15";

    public static void main(String[] args) throws Exception {
        byte[] bytes = Base64.getDecoder().decode(OLD_RECORD);
        String result;
        try (ObjectInputStream i = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
            i.readObject();
            result = "passed";
        } catch (InvalidClassException e) {
            result = "rejected: " + e.getClass().getSimpleName();
        }
        System.out.println("same old record with version ID 43: " + result);
    }
}
```

```
same old record with version ID 43: rejected: InvalidClassException
```

This time the same old record does not even get read that deep: because the version ID does
not match, the readback is rejected without ever looking at the fields. Placing the two
measurements side by side produces an inverted outcome. **Changing** the version ID stops an
incompatible record loudly. **Keeping** the version ID fixed — behaving as if the class never
changed at all — silently produces a wrong value once the shape has actually changed.
The serialization version ID is therefore not a version number, it is a single-bit answer to
the question "does this class definition recognize that stream"; the field names themselves
are part of that answer too, because the binding is made by name.

If the version ID had never been written at all, the outcome would not change, because in that
case the compiler computes the ID itself by looking at the class's current shape; two IDs
computed from two different shapes come out different almost every time, and the result would
again be `InvalidClassException`. Writing the version ID by hand and keeping it fixed is a
deliberate way of turning this automatic protection off — it is necessary if backward
compatibility is wanted, but wanting it also requires thinking through what **every** field
change will do to the readback. The same logic applies to an added field as well: a new field
that is not in the stream at all silently receives its own default value, exactly like
`amount` did.

#### Summary

- Serialization never runs the constructor; `ObjectInputStream` fills the fields directly, and
  even `final` fields can be written this way.
- Neither of the two invariants the constructor set up is preserved automatically on readback:
  a broken record already in hand reads back without raising any exception.
- Once a class-specific `readObject` method is added and the checks from the constructor are
  repeated, the same broken record is rejected; the check comes back because the caller wrote
  it.
- A field name is the binding key on readback: a field in the stream that is not in the class
  is ignored, and a field in the class that is not in the stream is silently filled with its
  default value.
- When the serialization version ID is kept fixed, a change in shape passes through silently;
  when the version ID is changed, the same mismatch is rejected loudly.
- `Serializable` is a marker interface and carries no abstract method; because of this, the
  compiler never checks whether a class also preserves its invariants on readback — the check
  depends entirely on the body of an optional `readObject`.

#### Next Step

Everything measured in this lesson was an in-memory, fixed byte stream — time never entered
the picture. But one of the most common kinds of data a system carries is time itself. The
next lesson looks at the date and time API: what information an instant, a local date-time,
and a zoned date-time each carry and each fail to carry, and how that difference can silently
lead to a wrong result when two values are compared.

### Date and Time API

Source: https://academia.sh/en/courses/java-standard-library/date-and-time-api

Measured by running whether each of three types (instant, local date-time, zoned date-time) can correctly answer the question 'does this show the same instant.' It shows that comparing without a time zone silently gives the wrong result, and that the gap and the overlap at a daylight saving transition are a place where the API picks a side by rule.

The previous lesson measured what happens when an object is turned into bytes — the constructor
never ran there, and time never entered the picture at all, because everything measured was an
in-memory, fixed byte stream. One of the most common kinds of data a system carries is time
itself, and time is represented by three separate types: `Instant` is an instant, `LocalDateTime`
is a local date-time, `ZonedDateTime` is a zoned date-time. These three types do not carry the
same information — one never knows the time zone, one always does — and this lesson measures
where that difference matters.

The course's question has not changed: which side holds a guarantee — here, the guarantee of
correctly answering "do these two values show the same instant"? This time the answer is not a
choice of class, it is a choice of **type**. Three sections proceed in order. The first tests all
three types against the same question. The second measures a concrete caller mistake in which
this question is answered wrongly. The third measures an edge case — a daylight saving
transition — where even adding a time zone does not answer every question.

These three are another form of what the previous lesson measured. The serialization lesson
measured **what information** a byte stream carries; the same question is asked here, except
what is being represented is not an object but time. A `LocalDateTime`, just like a record whose
field was renamed, silently produces a wrong result when used as though it carried information
it does not have — the difference is that there a serialization version ID was missing, here a
time zone is missing.

**Determinism note:** no real clock is ever read in this lesson. Every example is built on a
fixed `Instant` or on a date parsed from a fixed string; the daylight saving transitions are also
**discovered programmatically** from the system's own time zone data, no date is asserted in the
prose.

#### Three Types, One Question

- **IO22** — A single `Instant` is applied to two different time zones (Istanbul, New York),
  producing two `ZonedDateTime` values; these two values show **the same instant** but carry
  different local times. Four separate methods try to answer the same question ("are these two
  the same instant"): `Instant.equals`, `LocalDateTime.equals`, `ZonedDateTime.equals`,
  `ZonedDateTime.isEqual`.

```java
// ThreeTypes.java — which of the three types correctly answers "does this show the same instant"
import java.time.*;

public class ThreeTypes {
    public static void main(String[] args) {
        Instant instant = Instant.parse("2026-06-15T13:00:00Z");

        ZonedDateTime istanbul = instant.atZone(ZoneId.of("Europe/Istanbul"));
        ZonedDateTime newYork = instant.atZone(ZoneId.of("America/New_York"));
        LocalDateTime istanbulLocal = istanbul.toLocalDateTime();
        LocalDateTime newYorkLocal = newYork.toLocalDateTime();

        System.out.println("istanbul local time: " + istanbulLocal);
        System.out.println("new york local time: " + newYorkLocal);
        System.out.println();

        System.out.printf("%-42s%s%n", "question: does it show the same instant", "answer");
        System.out.printf("%-42s%s%n", "Instant.equals",
                instant.equals(Instant.parse("2026-06-15T13:00:00Z")));
        System.out.printf("%-42s%s%n", "LocalDateTime.equals (local times)",
                istanbulLocal.equals(newYorkLocal));
        System.out.printf("%-42s%s%n", "ZonedDateTime.equals (zone+local+offset)",
                istanbul.equals(newYork));
        System.out.printf("%-42s%s%n", "ZonedDateTime.isEqual (instant only)",
                istanbul.isEqual(newYork));
    }
}
```

```
istanbul local time: 2026-06-15T16:00
new york local time: 2026-06-15T09:00

question: does it show the same instant   answer
Instant.equals                            true
LocalDateTime.equals (local times)        false
ZonedDateTime.equals (zone+local+offset)  false
ZonedDateTime.isEqual (instant only)      true
```

Both `ZonedDateTime` values were produced from **the same** `Instant`, meaning both show
exactly the same instant — even though one reads 16:00 and the other 09:00. Two of the four
methods get this right: `Instant.equals` (which is already comparing the same instant) and
`ZonedDateTime.isEqual` (a method written specifically to compare only the instant a value
represents). `LocalDateTime.equals` says `false`, but it is not answering this question
wrongly — it **cannot answer it at all**, because the values it holds carry no information
about which time zone they belong to; it only says whether two calendar readings are equal to
each other. The most surprising line is the third: `ZonedDateTime.equals` says `false` even
though it holds time zone information, because that method answers not "is it the same
instant" but "are the zone, the local time, and the offset all exactly identical." Two methods
exist on the same type and they answer **different** questions; choosing the right one is left
to the caller.

These four results follow directly from what the three types carry. `Instant` is a single
point on a number line, and if two points are at the same place `equals` says so directly; it
has no other question to ask. `ZonedDateTime` carries that same point together with a calendar
reading as well — which zone, which local time — and `equals` folds this **extra** information
into the comparison too; even if two representations show the same instant, `equals` counts
them as different if their zones differ, because it defines the two values as **identical
representations**, not as the same instant. `isEqual` deliberately ignores this extra
information and looks only at the instant represented. `LocalDateTime`, on the other hand,
**never carries this extra information at all**; it has no zone, no offset to compare, and so
it answers not the question that was asked but the only question it can ask — whether two
calendar readings are equal.

#### The Caller's Rule: Comparing Without a Time Zone

- **IO23** — Two records carry the same local time value (`09:00`), but one was entered in
  Istanbul and one in New York; which time zone it was entered in is **not kept** together
  with the record. A comparison made without this information is run side by side with a
  comparison made with the information supplied.

This situation is not made up: a scheduling system, a log, a scheduled task — every system
that takes time in the user's own local time runs into the same choice. Storing the time zone
together with the record is a design decision, the language itself does not impose it;
`LocalDateTime` deliberately exists as a type with no time zone, because most of the time a
single calendar reading really is enough. The cost only shows up once that reading is used as
though it meant "the same instant."

```java
// MissingZone.java — comparing without a time zone silently gives the wrong result
import java.time.*;

public class MissingZone {
    static boolean naiveSameInstant(LocalDateTime a, LocalDateTime b) {
        return a.equals(b);
    }

    static boolean correctSameInstant(LocalDateTime a, ZoneId zoneA, LocalDateTime b, ZoneId zoneB) {
        return a.atZone(zoneA).toInstant().equals(b.atZone(zoneB).toInstant());
    }

    public static void main(String[] args) {
        LocalDateTime recordA = LocalDateTime.of(2026, 6, 15, 9, 0);
        LocalDateTime recordB = LocalDateTime.of(2026, 6, 15, 9, 0);
        ZoneId zoneA = ZoneId.of("Europe/Istanbul");
        ZoneId zoneB = ZoneId.of("America/New_York");

        System.out.println("record a (local): " + recordA + " (Istanbul)");
        System.out.println("record b (local): " + recordB + " (New York)");
        System.out.println("naive comparison (without a time zone): "
                + naiveSameInstant(recordA, recordB));
        System.out.println("correct comparison (with a time zone): "
                + correctSameInstant(recordA, zoneA, recordB, zoneB));

        Duration diff = Duration.between(recordA.atZone(zoneA).toInstant(),
                recordB.atZone(zoneB).toInstant());
        System.out.println("actual difference: " + diff.toHours() + " hours");
    }
}
```

```
record a (local): 2026-06-15T09:00 (Istanbul)
record b (local): 2026-06-15T09:00 (New York)
naive comparison (without a time zone): true
correct comparison (with a time zone): false
actual difference: 7 hours
```

The naive comparison says "same time" because the two `LocalDateTime` values really are equal
— both read `09:00`. But in reality there is a **seven-hour** difference between them. This is
not a flaw in the library: `LocalDateTime.equals` already says it is comparing only two
calendar readings and promises nothing else. The flaw is in using a type that **never** carries
time zone information as though it did. The correct comparison attaches a time zone to both
sides, converts to `Instant`, and only then compares — giving the right answer, `false`, and
the seven-hour difference underneath it. This is the direct consequence of the previous
section: `LocalDateTime` was the type that could not answer "is it the same instant"; here that
same gap takes concrete shape as a record-comparison mistake.

This rule is never enforced by the compiler. `naiveSameInstant` is a fully valid,
type-correct method that takes two `LocalDateTime` values and returns a `boolean`; the compiler
cannot know that it gives the wrong answer to a **semantically** wrong question, because all it
has is the fact that both parameters are the same class. The result takes the same shape as the
flaw measured in the previous lesson: the mistake does not even appear as an exception at
runtime, it just passes silently as a wrong `boolean`. The `Duration.between` call requires
both sides to be explicitly converted to an `Instant` to make this difference visible — meaning
the cost of writing the correct comparison is **not forgetting** the time zone, knowing all the
while that the compiler will give no warning if it is forgotten.

#### Gap and Overlap at a Daylight Saving Transition

- **IO24** — The system's New York time zone rules are queried for the first two transitions
  after a fixed starting instant. The first transition is a **gap** (a one-hour range of local
  time is never lived through), the second is an **overlap** (a one-hour range of local time is
  lived through twice). Which date this is is never asserted in the prose; the program uses
  whatever date it finds.

The previous two sections dealt with a time zone that was **missing**; this section measures
where the boundary lies even when the time zone is complete. `ZoneId.getRules()` returns a
`ZoneRules` object carrying all of a region's transition information; this information is not a
constant written into the code, it is read from the virtual machine's own time zone data.
`nextTransition` finds the first transition after a given instant — the result depends on which
instant is supplied, but this dependency does not make the measurement indeterminate, because
the starting instant here too is parsed from a fixed string.

```java
// RuleDiscovery.java — daylight saving transitions are discovered from the system's own time zone data
import java.time.*;
import java.time.zone.*;

public class RuleDiscovery {
    public static void main(String[] args) {
        ZoneId zone = ZoneId.of("America/New_York");
        ZoneRules rules = zone.getRules();
        Instant start = Instant.parse("2020-01-01T00:00:00Z");

        ZoneOffsetTransition firstTransition = rules.nextTransition(start);
        ZoneOffsetTransition secondTransition = rules.nextTransition(firstTransition.getInstant());

        System.out.println("is the first transition a gap: " + firstTransition.isGap());
        System.out.println("first transition local time before/after: "
                + firstTransition.getDateTimeBefore() + " / " + firstTransition.getDateTimeAfter());
        System.out.println("is the second transition an overlap: " + secondTransition.isOverlap());
        System.out.println("second transition local time before/after: "
                + secondTransition.getDateTimeBefore() + " / " + secondTransition.getDateTimeAfter());

        LocalDateTime insideGap = firstTransition.getDateTimeBefore().plusMinutes(30);
        ZonedDateTime gapResult = insideGap.atZone(zone);
        System.out.println();
        System.out.println("local time inside the gap: " + insideGap);
        System.out.println("default resolution: " + gapResult);
        System.out.println("was the local time pushed forward: "
                + !gapResult.toLocalDateTime().equals(insideGap));

        LocalDateTime insideOverlap = secondTransition.getDateTimeAfter();
        ZonedDateTime earlyOffset = insideOverlap.atZone(zone);
        ZonedDateTime lateOffset = earlyOffset.withLaterOffsetAtOverlap();
        System.out.println();
        System.out.println("local time inside the overlap: " + insideOverlap);
        System.out.println("default resolution (early offset): " + earlyOffset);
        System.out.println("when the later offset is requested: " + lateOffset);
        System.out.println("same local time, same instant: "
                + earlyOffset.toLocalDateTime().equals(lateOffset.toLocalDateTime()) + ", "
                + earlyOffset.toInstant().equals(lateOffset.toInstant()));
    }
}
```

```
is the first transition a gap: true
first transition local time before/after: 2020-03-08T02:00 / 2020-03-08T03:00
is the second transition an overlap: true
second transition local time before/after: 2020-11-01T02:00 / 2020-11-01T01:00

local time inside the gap: 2020-03-08T02:30
default resolution: 2020-03-08T03:30-04:00[America/New_York]
was the local time pushed forward: true

local time inside the overlap: 2020-11-01T01:00
default resolution (early offset): 2020-11-01T01:00-04:00[America/New_York]
when the later offset is requested: 2020-11-01T01:00-05:00[America/New_York]
same local time, same instant: true, false
```

Scanning the system's time zone rules forward from its own starting instant, the program finds
two transitions: one is a **gap**, one is an **overlap**. In the gap, the clock jumps straight
from 02:00 to 03:00 — a local time of `02:30` is never lived through in that region on that
day. So when we attach `02:30` to that zone, the API does not accept it **as given**; it pushes
it forward by the length of the gap, moving it to `03:30`. The overlap works the opposite way:
`01:00` is lived through **twice** that day, once under daylight time (offset −4), once under
standard time (offset −5). The API has to make a choice, and by default it picks the
**earlier** offset; when `withLaterOffsetAtOverlap` is called, the same local time is instead
attached to the real instant one hour **later** — the local time does not change, but the
instant it represents does.

Why the two cases turn out differently follows from the transition's own definition. In a gap
the clock genuinely **jumps** — after 02:00 the next real instant is read as 03:00 — so the only
real instant that corresponds to the local times in between is where the jump **ends**; that is
why the API pushes forward, not back. In an overlap the clock **winds back**: the same local
reading appears at two different real instants, and a choice has to be made between them because
both are equally valid. `getDateTimeBefore` and `getDateTimeAfter`, the two methods used in this
measurement, say **which direction** the transition runs: in a gap the second value is greater
than the first, in an overlap it is smaller — whether a transition is a gap or an overlap can be
read off from this too, `isGap`/`isOverlap` only names it.

This section's limiting measurement is this: adding a time zone solves the mistake from the
previous section, but it does not answer **every** question. In the overlap case, even the
local time that comes with a zone is not enough on its own — the same local time, in the same
zone, can correspond to two different real instants, and the API itself has to choose which one
is meant. This choice is not arbitrary: the system's time zone rules define which local times
do not exist and which exist twice, and the API picks **one side** according to that rule. The
choice itself is fixed and documented; but it may not be the side the caller wanted, and the
only way to notice is to use queries like `isGap`/`isOverlap`.

#### Summary

- `Instant` is an instant and carries no time zone; `LocalDateTime` is a local date-time and
  carries no time zone; `ZonedDateTime` is a zoned date-time and does carry a time zone.
- `Instant.equals` and `ZonedDateTime.isEqual` correctly answer "does it show the same
  instant"; `LocalDateTime.equals` cannot answer this question at all, and `ZonedDateTime.equals`
  answers a different question (are the zone, the local time, and the offset all exactly
  identical).
- Two local date-times compared without a time zone can come out equal even when a real
  difference exists between them; the flaw is not in the library, it is in assuming information
  the type does not carry.
- In the gap of a daylight saving transition, a range of local time is never lived through and
  the API pushes it forward by the length of the transition; in the overlap, a range of local
  time is lived through twice and the API picks the earlier offset by default.
- The limiting measurement: adding a time zone does not answer every question. An overlapping
  local time can correspond to two different real instants even within the same zone, and which
  one is meant can only be understood with queries like `isGap`/`isOverlap`.
- Gap and overlap come from the direction of the transition: when the clock jumps forward, the
  local times in between are never lived through; when the clock winds back, a range of local
  time is lived through twice.

#### Next Step

Everything measured in this lesson was numeric and calendar-based. But the standard library's
input-output layer also has a **text**-matching question: asking whether a string matches a
given pattern. The next and final lesson of the course looks at regular expressions — showing
that a pattern is a compiled value, that a greedy and a reluctant quantifier produce different
captures on the same input, and that backtracking is measured by its number of steps.

### Regular Expressions

Source: https://academia.sh/en/courses/java-standard-library/regular-expressions

Measures that a pattern is a compiled value, that exact matching and searching carry separate contracts, and that a greedy and a reluctant quantifier produce different captures on the same input. Shows that recompiling the pattern on every call is ineffective, and that backtracking's step count does not grow linearly with the input.

The previous lesson measured which information each of the three time types carries and which
it does not. This lesson asks the same question on **text**: whether a string matches a given
pattern. Java's regular expression API has two parts — `Pattern`, the pattern itself; `Matcher`,
the party that applies a pattern to a specific input — and this split already hints at the
answer: a pattern is a **value** built once and used again and again, not a one-time call.

Four sections proceed in order: the pattern as a value and the two matching methods' separate
contract, the separate result two quantifier forms produce on the same input, the course's
single **ineffective** rule break (broken rule, unchanged result, changed cost), and the final
limiting measurement — backtracking's step count does **not grow linearly** with the input.

Regular-expression syntax was already built up in the Shell Programming course and in Python
Fundamentals, and is not retaught here. This lesson's contribution is Java's own mechanism: the
pattern as a **compiled object**, matching called through separate methods on it, and the
decisions those methods force onto the caller.

#### A Pattern Is a Compiled Value

- **IO25** — A single `Pattern` object is tested against four separate inputs with both
  `matches` (exact match) and `find` (search). Same pattern, same input, two separate methods.

```java
// ExactMatchAndSearch.java — the pattern is a compiled value, matches and find carry separate contracts
import java.util.regex.*;

public class ExactMatchAndSearch {
    public static void main(String[] args) {
        Pattern pattern = Pattern.compile("[0-9]+");
        String[] inputs = { "12345", "code-12345", "12345-end", "abc" };

        System.out.printf("%-16s%-10s%s%n", "input", "matches", "find");
        for (String input : inputs) {
            Matcher m1 = pattern.matcher(input);
            boolean exactMatch = m1.matches();
            Matcher m2 = pattern.matcher(input);
            boolean search = m2.find();
            System.out.printf("%-16s%-10s%s%n", input, exactMatch, search);
        }
    }
}
```

```
input           matches   find
12345           true      true
code-12345      false     true
12345-end       false     true
abc             false     false
```

The `Pattern.compile` call is made once, and the returned `pattern` becomes the basis for four
separate `Matcher` instances — the pattern itself is not a call, it is a **value**. The two
methods called on it ask different questions. `matches` asks whether the **entire** input fits
the pattern; `code-12345` has digits, but the extra `code-` prefix means the whole input does
not fit, so `matches` returns `false`. `find`, on the other hand, looks for a substring
**somewhere** in the input that fits the pattern; since `12345` is found inside the same input,
`find` returns `true`. Only on the last line do the two agree, because `abc` has no digits at
all — neither method can find anything. What changes the contract while `Pattern` stays the
same is the method called, not the pattern itself.

#### A Greedy and a Reluctant Quantifier Produce Different Captures

- **IO26** — The same capturing group is written in two quantifier forms: `(.*)` greedy,
  `(.*?)` reluctant. Both are applied to the same input, the same pair of square brackets.

```java
// GreedyAndReluctant.java — a greedy and a reluctant quantifier produce different captures on the same input
import java.util.regex.*;

public class GreedyAndReluctant {
    public static void main(String[] args) {
        String input = "[record-1][record-2]";

        Pattern greedy = Pattern.compile("\\[(.*)\\]");
        Pattern reluctant = Pattern.compile("\\[(.*?)\\]");

        Matcher m1 = greedy.matcher(input);
        m1.find();
        Matcher m2 = reluctant.matcher(input);
        m2.find();

        System.out.println("input: " + input);
        System.out.println("greedy    [(.*)]   captured: " + m1.group(1));
        System.out.println("reluctant [(.*?)]  captured: " + m2.group(1));

        int greedyCount = 0;
        Matcher m3 = greedy.matcher(input);
        while (m3.find()) greedyCount++;
        int reluctantCount = 0;
        Matcher m4 = reluctant.matcher(input);
        while (m4.find()) reluctantCount++;
        System.out.println("greedy total match count: " + greedyCount);
        System.out.println("reluctant total match count: " + reluctantCount);
    }
}
```

```
input: [record-1][record-2]
greedy    [(.*)]   captured: record-1][record-2
reluctant [(.*?)]  captured: record-1
greedy total match count: 1
reluctant total match count: 2
```

Both quantifiers mean "zero or more characters" and accept **exactly the same** set. What
differs is not the set but the **order** they try it in. Greedy tries the longest run first and
backs off only when the match fails; it starts with a `.*` reaching the end of the input, then
gives ground backward until a closing bracket appears. Since the input has two closing brackets,
the last one wins — the captured string spans everything between the two records,
`record-1][record-2`. Reluctant does the opposite: it tries the shortest run, the empty string,
first, and extends only when it must, stopping at the first closing bracket. The concrete result
shows up in the match count: greedy consumes the whole input in **one** match, reluctant finds
**two** separate matches in the same input.

#### The Caller's Rule: Recompiling Is Ineffective

- **IO27** — Five inputs are processed two ways: with a pattern recompiled every time, and
  with a pattern compiled once and used five times. The number of compile calls is also
  counted.

```java
// Recompiling.java — recompiling the pattern on every call is ineffective: result stays the same, cost does not
import java.util.List;
import java.util.regex.*;

public class Recompiling {
    static int compileCounter = 0;

    static Pattern compile(String regex) {
        compileCounter++;
        return Pattern.compile(regex);
    }

    static boolean compilingEveryCall(String regex, String input) {
        return compile(regex).matcher(input).find();
    }

    public static void main(String[] args) {
        List<String> inputs = List.of("alpha-01", "beta-02", "gamma-03", "delta-04", "epsilon-05");
        String regex = "^[a-z]+-[0-9]{2}$";

        compileCounter = 0;
        boolean[] results1 = new boolean[inputs.size()];
        for (int i = 0; i < inputs.size(); i++)
            results1[i] = compilingEveryCall(regex, inputs.get(i));
        int compileCountEveryCall = compileCounter;

        compileCounter = 0;
        Pattern singlePattern = compile(regex);
        boolean[] results2 = new boolean[inputs.size()];
        for (int i = 0; i < inputs.size(); i++)
            results2[i] = singlePattern.matcher(inputs.get(i)).find();
        int compileCountOnce = compileCounter;

        System.out.println("are the results the same: " + java.util.Arrays.equals(results1, results2));
        System.out.println("compiling every call - compile count: " + compileCountEveryCall);
        System.out.println("compiling once       - compile count: " + compileCountOnce);
    }
}
```

```
are the results the same: true
compiling every call - compile count: 5
compiling once       - compile count: 1
```

This measurement is an example of the course's third class: an **ineffective** rule break. Both
paths reach exactly the same result across all five inputs — which fits and which does not is
identical either way. Recompiling breaks the caller's rule ("a reused pattern should be compiled
once"), but the break neither throws an exception nor silently produces a wrong result;
**correctness is never disturbed**. Only the cost is: over five inputs, the compile calls go
from one to five. `Pattern.compile` may look cheap for a small pattern, but compiling is real
work turning pattern text into a state machine, redone on every call. The break stays invisible
here, since no output line is wrong — only the work spent is excessive.

The course's shared definitions named three classes: `exception` (falls immediately, by name),
`silent` (wrong result, no complaint), `ineffective` (the rule concerns cost, not correctness).
Fourteen of fifteen observations fell into the first two; this is the fifteenth, and the
course's only `ineffective` line — the **easiest to miss** break of all, since it leaves no
visible trace, only a difference in work between two pieces of code that do the same thing.

#### The Limiting Measurement: Backtracking Step Count Does Not Grow Linearly

- **IO28** — The input is wrapped in a sequence that counts its own `charAt` calls. The same
  pattern is applied to an input that keeps growing but never matches; the total read count is
  recorded on each trial. The measure is the **step count, not the duration**.

```java
// CountingSequence.java — a CharSequence that wraps an input and counts its own charAt calls (helper class, no main)
class CountingSequence implements CharSequence {
    private final String base;
    long readCount = 0;

    CountingSequence(String base) { this.base = base; }

    @Override public int length() { return base.length(); }

    @Override public char charAt(int i) {
        readCount++;
        return base.charAt(i);
    }

    @Override public CharSequence subSequence(int b, int e) { return base.subSequence(b, e); }

    @Override public String toString() { return base; }
}
```

```java
// BacktrackingSteps.java — in backtracking matching, step count does not grow linearly with input
import java.util.Locale;
import java.util.regex.Pattern;

public class BacktrackingSteps {
    static long countSteps(Pattern p, String input) {
        CountingSequence seq = new CountingSequence(input);
        p.matcher(seq).matches();
        return seq.readCount;
    }

    public static void main(String[] args) {
        Pattern pattern = Pattern.compile("(a+)+b");

        System.out.printf(Locale.ROOT, "%-10s%s%n", "length", "charAt call count");
        long previous = 0;
        for (int length = 10; length <= 25; length += 5) {
            String input = "a".repeat(length);
            long steps = countSteps(pattern, input);
            System.out.printf(Locale.ROOT, "%-10d%d%n", length, steps);
            if (previous > 0) {
                double ratio = (double) steps / previous;
                System.out.printf(Locale.ROOT, "  (growth ratio over previous length: %.1f)%n", ratio);
            }
            previous = steps;
        }
    }
}
```

```
length    charAt call count
10        100
15        225
  (growth ratio over previous length: 2.3)
20        400
  (growth ratio over previous length: 1.8)
25        625
  (growth ratio over previous length: 1.6)
```

The input is made up of the letter `a` and contains no `b`; the pattern `(a+)+b` requires a `b`
at the end no matter what. The match is doomed from the start, but until the engine works this
out, the inner group `(a+)` explores **how many ways** it can split the input into runs — one
letter, two, three — and every time a split fails to find a `b`, it backs off and tries another
split. As input length grows from 10 to 25, two and a half times over, the read count grows
from 100 to 625, that is 6.25 times over — the square of two and a half: 100 at length 10,
225 at 15, 400 at 20, 625 at 25 —
each line grows **proportionally to the square** of the previous one. While input grows linearly
(constant step, constant increase), the step count grows quadratically; this is the measured
shape of "does not grow linearly with the input." The measure is not a real stopwatch but a
wrapper counting `charAt` calls — which is why the result comes out **exactly the same** no
matter who runs this document or on which machine.

The `(a+)+b` pattern itself is a valid, ordinary regular expression; what makes it
**dangerous** is not an escape sequence or some special trick but that a nested quantifier can
group the same characters in more than one way — this lesson does not build that geometry, it
only counts it.

#### Summary

- `Pattern` is a compiled value; `matches` (exact match) and `find` (search) carry separate
  contracts on the same pattern and can give separate results on the same input.
- The greedy quantifier (`.*`) tries the longest run first and narrows backward; the reluctant
  quantifier (`.*?`) tries the shortest run first and extends as needed; they produce separate
  captures and separate match counts on the same input.
- Recompiling the pattern on every call is the course's only `ineffective` rule break: the match
  result does not change, only the compile call count grows.
- In backtracking matching, the step count may not grow linearly with the input; in the
  measured example, as the input length grew two and a half times, the step count grew
  6.25 times — exactly the square of two and a half.
- The step count was measured not with a real stopwatch but with a `CharSequence` that wraps the
  input and counts its read calls; the result is therefore machine-independent and reproducible.

#### Course Wrap-Up

The course opened with one question: the moment you call a library someone else wrote, the
decision moves outside your own language — so who gives the promise it makes you? Fifteen
lessons put this question to fifteen behaviors, each time getting the answer by running at
least two implementations, or two calling forms, side by side. The main claim was confirmed by a
count in the very first lesson: of the fifteen observations measured across the collection
framework, seven were interface guarantees, eight were behavior of only the chosen
implementation — most of a behavior list drawn from a single class was not the interface's
promise. The second claim was confirmed too: when the caller's rule was broken, what fell most
often was not an exception but a silently wrong result. The third claim was confirmed along
with its limit: the compiler's protection does not end entirely, but what it protects changes.
The single-abstract-method restriction and the effectively `final` rule still fall at compile
time; both are promises about a **type's shape**. But none of the caller obligations the course
measured showed up at compile time — all of it compiled, and the distinction was left to
runtime. The compiler verifies a type's shape, not that the promise was kept.

| Lesson | Behavior measured | Who gives the guarantee | Limiting measurement |
|---|---|---|---|
| Collections Framework | 7 of 15 observations are interface guarantees, 8 implementation behavior | interface (7) + implementation (8) | With a 4-element source, `PriorityQueue`'s traversal order coincidentally matches output order; the 5th element opens the gap |
| List, Set, and Queue | From a 6-element input, list drops no information, set drops 3, queue drops only positional access | interface (dropped information is the promise's cost) + caller (`remove(1)`) | `LinkedList` speaks two interfaces at once; the same `remove(1)` carries two meanings, and the break is silent |
| Maps | Three promises (key uniqueness, live view, absent key gives null) hold across three implementations; order and null keys diverge | interface (3 promises) + implementation (order) + caller (`equals`/`hashCode`) | The same flawed key is found in a sorted map; the promise used there is `compareTo` |
| Iterators and Concurrent Modification | `NoSuchElementException` on exhaustion is the same across three implementations; response to a change during traversal differs across three | interface (exhaustion) + implementation (response to change) | `Iterator.remove` does not break the rule; the rule is "no modification from outside the iterator" |
| Comparison and Sorting | Natural order is the type's own promise, a comparator a separate object the caller supplies; stability holds across two implementations | interface (stability) + caller (comparator consistency) | An inconsistent comparator never drops an element from the list; the flaw comes from the container using it as a uniqueness decision |
| Functional Interfaces | The single abstract method is a compile-time promise; the behavioral promise is never checked anywhere | interface (the type's shape); the behavioral promise belongs to no one | The annotation adds no guarantee, it only catches a violation earlier; an unannotated interface can be functional too |
| Lambda Expressions | Captured by the local variable's value, effectively `final` enforced at compile time; a field carries no such restriction | caller (which form is chosen) | The restriction freezes the variable, not the object it points to; if a captured list changes, the lambda sees the new content |
| Stream API | 3 of ten measurements are guarantees independent of the source, 5 depend on source ordering, 2 are the caller's rule | interface (3) + source (5) + caller (2) | The sentence "the stream preserves order" is the source's promise, not the stream's |
| Collectors | Four parts (supplier, accumulator, combiner, finisher); the result container's type is not the interface's promise | implementation (container type) + caller (duplicate key) | When the caller supplies the container factory, the ambiguity closes; a dropped promise can be taken back |
| `Optional` | Unchecked-call count is equal across both signatures (4/4); the gain is visibility in the signature | caller (the check stays at runtime) | The gain disappears in a field and a parameter; `of(null)` fails immediately, `ofNullable(null)` does not |
| Stream-Based Input/Output | `write(int)`'s low-eight-bits promise and `read()`'s 0-255-plus-`-1` promise hold across two implementations; buffering only changes call count | interface (the byte promise) + caller (flushing) | An unflushed buffer leaves data incomplete; the decision to flush belongs to the caller |
| File System API | Path algebra is the same across two providers; `exists` and `size` query the provider's own storage | interface (path algebra) + implementation (storage) + caller (two steps) | An existence check and an open are separate operations; if the file is deleted in between, the check passes and the open fails |
| Serialization | The constructor never runs; zero of the two invariants it set up hold automatically on readback | caller (no check without a written `readObject`) | With the version ID kept fixed and a field renamed, the old record silently passes with a default value |
| Date and Time API | `Instant.equals` and `ZonedDateTime.isEqual` answer "is it the same instant"; `LocalDateTime.equals` cannot answer it | type (information carried) + caller (comparing without a time zone) | At a daylight saving transition, an overlapping local time maps to two separate instants; the API picks the earlier offset |
| Regular Expressions | `Pattern` is a compiled value; `matches` and `find` carry separate contracts, greedy and reluctant produce separate captures | interface (`Pattern`'s contract) + caller (recompiling: the only `ineffective` line) | In backtracking, step count does not grow linearly with input; the measure is step count, not duration |

The Standard Library and Streams closes here. The next course, **JVM, Concurrency, and
Performance**, turns the same questions on a third layer: behind a guarantee there is no
longer a single thread but a runtime in which multiple threads reach the same object at the
same time — and at that point the answer to "who gives the guarantee" can start to depend on
timing.

