# Java From Zero

> Learn Java from nothing to genuinely advanced: install the JDK and the basics - types, classes, objects, collections - then the deep half: generics, lambdas and streams, modern records and pattern matching, concurrency, the JVM and its garbage collector, testing, and performance. Mental-model-first, with clear explanations.


---

# Java From Zero

Java has been quietly running the world for thirty years. It's behind Android apps, the backends of
most large banks and enterprises, Minecraft, and an enormous share of the systems that move money and
data around the planet. That longevity comes from a deliberate bet: write once, run anywhere - your code
compiles to bytecode that runs on the **JVM** (Java Virtual Machine), the same on a laptop, a server, or
a phone. Java values stability, readability, and tooling over cleverness, which is exactly why teams keep
choosing it for software that has to keep working for a decade.

This guide takes you the whole way: from "I've never run `javac`" to understanding what Java and its
runtime are *actually doing* underneath your code. We go mental-model-first the whole way: before any
command, you'll understand what the thing actually *is* and why Java made the choice it did.

> 📝 This guide teaches the **language**. If you've never programmed at all, start with
> [Programming From Zero](/guides/programming-from-zero) first - it covers the universal ideas (what a
> program is, variables, loops) every language shares. Then come back here.

It's one zero-to-hero journey in two halves. **Phases 1–9 are the basics** - enough to write real,
well-organized object-oriented programs. **Phases 10–17 are the deep half** - generics, lambdas and
streams, modern Java (records, sealed types, pattern matching), concurrency, the JVM and garbage
collector, testing, and performance, the stuff that separates "writes Java" from "understands Java."
Each phase carries a difficulty badge so you can see the climb.

## How to read this

- **Brand new to Java?** Read 1–9 in order, top to bottom - each builds on the last. Type the examples
  yourself; doing beats reading. Come back for 10+ when the basics feel comfortable.
- **Already know another language?** Skim phases 1–4 for Java's spelling of ideas you have, then slow
  down at [Phase 5: Classes & Objects](05-classes-and-objects.md) - object-orientation is Java's whole
  worldview, and everything after it assumes you think in objects.
- **Past the basics already?** Jump to the deep half - [Phase 10: Generics, Deep](10-generics-deep.md)
  onward is where Java stops being "a verbose OOP language" and becomes one you can reason about down to
  the garbage collector.

## The phases

**Part 1 - The basics (🟢 Basic → 🟡 Intermediate)**
1. **[Install & Your First Program](01-install-and-first-program.md)** 🟢 - the JDK, `javac`/`java`, the JVM, a real `Hello`.
2. **[Syntax, Values & Types](02-syntax-values-and-types.md)** 🟢 - primitives vs objects, `var`, static typing, strings.
3. **[Collections](03-collections.md)** 🟢 - arrays, `List`/`ArrayList`, `Map`/`HashMap`, `Set`, and generics for collections.
4. **[Control Flow & Methods](04-control-flow-and-methods.md)** 🟢 - `if`/`switch`/loops, methods, and overloading.
5. **[Classes & Objects](05-classes-and-objects.md)** 🟡 - **Java's whole worldview:** classes, fields, constructors, `this`, encapsulation.
6. **[Inheritance & Interfaces](06-inheritance-and-interfaces.md)** 🟡 - `extends`, interfaces, polymorphism, and `abstract`.
7. **[Errors & I/O](07-errors-and-io.md)** 🟡 - checked vs unchecked exceptions, `try`/`catch`/`finally`, try-with-resources, files.
8. **[Packages, Build & Tooling](08-packages-and-tooling.md)** 🟡 - packages, the classpath, Maven/Gradle, JARs, the ecosystem.
9. **[Idioms & Gotchas](09-idioms-and-gotchas.md)** 🟡 - the Java way, and the traps (`null`, `==` vs `equals`, autoboxing) that bite everyone once.

**Part 2 - Beyond the basics (🔴 Advanced)**
10. **[Generics, Deep](10-generics-deep.md)** 🔴 - bounded types, wildcards (`? extends`/`super`), and type erasure.
11. **[Lambdas & Functional Interfaces](11-lambdas-and-functional-interfaces.md)** 🟡 - lambdas, method references, and the functional-interface system.
12. **[The Streams API](12-streams-api.md)** 🟡 - declarative pipelines: `map`/`filter`/`collect`, laziness, and parallel streams.
13. **[Records, Sealed Types & Modern Java](13-records-and-modern-java.md)** 🟡 - records, sealed classes, switch expressions, pattern matching, `Optional`.
14. **[Concurrency & Threads](14-concurrency-and-threads.md)** 🔴 - threads, `synchronized`, the memory model, `ExecutorService`, `CompletableFuture`, virtual threads.
15. **[The JVM: Memory, GC & JIT](15-the-jvm-memory-and-gc.md)** 🔴 - heap vs stack, the garbage collector, JIT compilation, and class loading.
16. **[Testing, Build & Profiling](16-testing-and-profiling.md)** 🟡 - JUnit, table-driven tests, Maven/Gradle deeper, and profiling with JFR.
17. **[Performance & the Ecosystem](17-performance-and-ecosystem.md)** 🔴 - cutting allocation/GC pressure, measuring first, and the Java ecosystem.

**Finale**
18. **[Where to Go Next](18-where-to-go-next.md)** 🟢 - Spring Boot, Android, big data, and what to build.

> Frameworks (Spring Boot, Android, Jakarta EE) are their own world - this guide makes the *language and
> the JVM* make sense, top to bottom.


---

# Install & Your First Program - The JDK, javac & the JVM

Every language asks two things before anything fun happens: get the tools onto your machine, and prove
they work by running one tiny program. Java has a twist in that first program - it doesn't run the way
many languages do, and understanding that twist now saves a hundred moments of confusion later.

## The big idea: Java compiles to bytecode that runs on the JVM

When you "run" a Java program, two separate things happen, potentially on *different machines at different
times*:

1. **Compile.** A tool called `javac` reads your human-written `.java` file and translates it into
   **bytecode** - a compact, portable instruction set - saved as a `.class` file. Bytecode is *not*
   machine code for your CPU; it's instructions for an imaginary computer.
2. **Run.** A program called the **JVM** (Java Virtual Machine) loads that `.class` file and *executes*
   the bytecode, translating it into real instructions for whatever CPU it happens to be sitting on.

That imaginary computer is the key trick: code compiled once into bytecode runs on *any* machine with a
JVM - Windows, macOS, Linux, your phone, a bank mainframe. This is the famous Java promise: **write once,
run anywhere**. You compile for the JVM, and the JVM is the part that's been ported everywhere.

```mermaid
flowchart LR
  A[Hello.java<br/>source code] -->|javac| B[Hello.class<br/>bytecode]
  B -->|java| C[JVM]
  C --> D[runs on<br/>any OS / CPU]
```

*What just happened:* The diagram traces the whole pipeline: `.java` source, `javac` to portable `.class`
bytecode, `java` hands that to the JVM, which runs it on the actual hardware. The `.class` file is the
portable artifact - the same one runs on every JVM.

Three acronyms get mixed up constantly. Pin them down once:

📝 **JVM** (Java Virtual Machine) - the engine that *executes* bytecode. It's the "imaginary computer"
your code targets. **JRE** (Java Runtime Environment) - the JVM *plus* the standard libraries your code
needs to run; everything required to *run* a Java program, nothing to *build* one. **JDK** (Java
Development Kit) - the JRE *plus* the developer tools: the `javac` compiler, the `java` launcher, a
debugger, and more. Everything required to *build and run*.

💡 **Key point.** You want the **JDK** - the superset, JVM and libraries included, so it gives you the
compiler *and* the runtime in one go. "Installing just a JRE" only makes sense for shipping software to end
users who run it but don't build it; as a learner, the JDK is your one download.

## Install a JDK

Java is open source; several distributions ship essentially the same thing. The two names you'll see most:

- **OpenJDK** - the open-source reference implementation. Most distributions are builds of this.
- **Oracle JDK** - Oracle's own build. Functionally near-identical for learning, but shifting licensing
  terms are why most people reach for a free OpenJDK build instead.

A friendly, free, no-licensing-drama OpenJDK build is **Eclipse Temurin** from
[adoptium.net](https://adoptium.net): grab the installer for your OS (Windows `.msi`, macOS `.pkg`, or a
Linux package) and choose a **LTS** (Long-Term Support) version like 25 (the newest LTS). Accept the installer's defaults;
on Windows, let it add Java to your `PATH` when offered.

Confirm both halves of the JDK are present - the runtime *and* the compiler:

```bash
java -version
javac -version
```
```console
$ java -version
openjdk version "25.0.1" 2025-10-21
OpenJDK Runtime Environment Temurin-25.0.1+9 (build 25.0.1+9)
OpenJDK 64-Bit Server VM Temurin-25.0.1+9 (build 25.0.1+9, mixed mode)

$ javac -version
javac 25.0.1
```
*What just happened:* `java -version` reports the **runtime**; `javac -version` reports the **compiler**.
If *both* answer with a version, you have a complete JDK. The exact numbers will differ over time -
anything Java 17 or newer works for this guide.

⚠️ **Gotcha.** If `java -version` works but `javac -version` says "command not found" / "is not
recognized," you've installed a *JRE only* - you can run Java but not compile it. Reinstall the **JDK**
from Adoptium. If a terminal already open before you installed says `java` isn't found, open a fresh one -
old terminals don't know about the new `PATH`.

## Your first program

Make a file called `Hello.java` - the capital `H` matters, as you'll see shortly - in any folder, with
exactly this:

```java
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, Java!");
    }
}
```
*What just happened:* That's a complete Java program - a lot of words for one line of output, but Java is
wordy by design. Naming each piece:

- `public class Hello` - Java organizes all code into **classes** (named bundles of code and data). Every
  program needs at least one; `public` means "visible from everywhere." Think of the class as the
  container your code lives inside.
- `public static void main(String[] args)` - defines a **method** (a named block of code) called `main`.
  This exact signature is the **entry point**, the one method the JVM looks for and runs when your program
  starts.
- `System.out.println("Hello, Java!")` - calls `println` ("print line") to write text to the screen,
  followed by a newline. `System.out` is the standard output stream that ships with Java.

That `main` signature trips everyone up at first, so briefly, *why* it reads this way: the JVM needs one
agreed-upon "start here" method. `public` so the JVM can reach it from outside the class; `static` so it
can be called *without first creating an object* (more in [Phase 5](05-classes-and-objects.md)); `void`
because it returns nothing; `String[] args` to receive command-line arguments. You'll type this line so
often it becomes muscle memory - just know every part has a job.

Now compile it. From the folder containing `Hello.java`:

```bash
javac Hello.java
```
```console
$ javac Hello.java
$ ls
Hello.class  Hello.java
```
*What just happened:* `javac` read your source and, on success, said *nothing* - silence is good news in
the Unix tradition. It produced `Hello.class` next to your source: portable bytecode for the JVM, not for
your CPU. (Not human-readable - open it and you'll see gibberish.)

Then run it - and note you pass `Hello`, the class name, **not** `Hello.class` or `Hello.java`:

```bash
java Hello
```
```console
$ java Hello
Hello, Java!
```
*What just happened:* `java` launched a JVM, loaded the `Hello` class, found `main`, and ran it top to
bottom, printing your line. You give it the *class* name because the JVM thinks in classes, not files; it
finds `Hello.class` on its own. That two-step rhythm - `javac` to build, `java` to run - is the bedrock
workflow under every Java tool you'll ever use.

## The "everything lives in a class" rule

Notice there's no loose code floating at the top of the file - no statement outside a class. That's not a
style choice; it's a rule. In classic Java, *every* line of code lives inside a class - no free-floating
functions or top-level statements like Python or JavaScript allow.

⚠️ **Gotcha - the filename must match the public class.** A file containing `public class Hello` *must*
be named `Hello.java` - same name, same capitalization. Name it `hello.java` or `Main.java` and `javac`
refuses to compile it, with an error like `class Hello is public, should be declared in a file named
Hello.java`. This catches everyone once. The rule exists so the compiler can find any public class by
predicting its filename - but the takeaway is simpler: **public class name = filename**, capital letter
and all.

A note on the future: recent Java *loosens* this, letting you write a stripped-down program
with just a `main` method and no visible class wrapper, to ease beginners in. It was a preview feature in
Java 21-24 and became standard in Java 25 (JEP 512). Even so, essentially all existing Java code uses the
classic `public class { ... }` form, which is what this guide uses and what "Java" means in practice today.

## A smoother workflow

The `javac` → `java` two-step is the real foundation, worth doing by hand once so the pipeline isn't
magic. Day to day, you'll lean on shortcuts:

- **Single-file run (Java 11+).** For a one-file program, you can skip the separate compile step entirely:

  ```bash
  java Hello.java
  ```
  ```console
  $ java Hello.java
  Hello, Java!
  ```
  *What just happened:* Handed a `.java` file directly, `java` compiled it *in memory* and ran it
  immediately - no `.class` file left on disk. Fastest way to try a quick idea; under the hood it's still
  compile-then-run, you just skipped typing both commands.

- **Real projects use a build tool.** Once a project grows past a file or two - multiple classes, outside
  libraries, tests - a **build tool** like **Maven** or **Gradle** manages compiling, dependencies, and
  packaging instead of you calling `javac` by hand. Covered in [Phase 8](08-packages-and-tooling.md); for
  now plain `javac`/`java` keeps the focus on the language.

- **And an IDE.** Almost everyone writes Java in an IDE - **IntelliJ IDEA** (community edition is free) or
  **VS Code** with Java extensions are the popular picks. They compile as you type, underline mistakes
  before you run, and turn build-and-run into one button. Pick either.

## Recap

1. Java **compiles to bytecode that runs on the JVM** - `javac` turns `.java` into a portable `.class`,
   and the JVM runs that bytecode anywhere, which is what "write once, run anywhere" means.
2. **JDK vs JRE vs JVM:** the JVM *executes* bytecode, the JRE is the JVM plus standard libraries (enough
   to *run*), and the JDK is the JRE plus dev tools like `javac` (enough to *build*). Install the **JDK**.
3. Confirm your install with **`java -version`** (runtime) and **`javac -version`** (compiler) - you need
   *both* to answer.
4. A program needs a **class** and the special **`public static void main(String[] args)`** entry point;
   build with **`javac Hello.java`**, then run with **`java Hello`** (the class name, not the filename).
5. ⚠️ **Everything lives in a class**, and the **public class name must match the filename** exactly -
   `public class Hello` belongs in `Hello.java`.
6. Shortcuts for later: **`java Hello.java`** runs a single file directly (Java 11+), and real projects
   use a **build tool** (Maven/Gradle) plus an **IDE** (IntelliJ/VS Code).

Next: variables, the values they hold, and Java's type system - the rules that decide what fits in what.

## Quick check

Test yourself on the one idea that makes Java *Java* - compile to bytecode, run on the JVM:

```quiz
[
  {
    "q": "What does `javac` produce, and what runs it?",
    "choices": [
      "It produces a `.class` bytecode file, which the JVM (via the `java` command) executes",
      "It produces a native CPU executable that the OS runs directly",
      "It runs the program immediately and produces no file",
      "It produces another `.java` file optimized for speed"
    ],
    "answer": 0,
    "explain": "`javac` compiles your `.java` source into portable `.class` bytecode. That bytecode isn't machine code - the JVM, launched by the `java` command, loads and executes it on whatever hardware it's running on. That's the compile-then-run, write-once-run-anywhere model."
  },
  {
    "q": "You have a JDK installed. Which is true about JDK, JRE, and JVM?",
    "choices": [
      "The JDK includes a JRE (and thus a JVM) plus developer tools like `javac`",
      "The JRE includes the JDK, so installing a JRE gives you the compiler",
      "The JVM is the largest package and contains both the JDK and JRE",
      "They are three unrelated downloads you must install separately"
    ],
    "answer": 0,
    "explain": "They nest: the JVM executes bytecode; the JRE is the JVM plus standard libraries (enough to run); the JDK is the JRE plus build tools like `javac` (enough to build and run). Installing the JDK gives you everything."
  },
  {
    "q": "Your file contains `public class Hello`. What must the file be named?",
    "choices": [
      "`Hello.java` - the filename must match the public class name exactly, capitalization included",
      "`hello.java` - Java filenames are always lowercase",
      "`Main.java` - the file holding `main` must be called Main",
      "Anything ending in `.java` - the name doesn't matter"
    ],
    "answer": 0,
    "explain": "A public class must live in a file whose name matches it exactly, including the capital letter - `public class Hello` requires `Hello.java`. Mismatch it and `javac` refuses to compile, telling you the file should be named after the class."
  }
]
```


---

# Syntax, Values & Types - Primitives, Objects & Static Typing

A program that only prints a fixed greeting is a dead end. Real programs hold values - a name, a count, a
price. In Java, every value has a **type**, enforced strictly enough to feel like paperwork at first - but
it becomes a safety net fast.

The idea that organizes this phase: Java draws a hard line between **primitives** (a small set of types
holding a raw number or true/false directly) and **objects** (everything else, reached through a
reference). Almost every surprise here traces back to which side of that line you're standing on.

## What "statically typed" actually means

**What it is.** Java is **statically typed**: every variable has a fixed type that's written down and
checked when your code is *compiled*, before it ever runs. A variable declared to hold a whole number can
never later hold a piece of text. The compiler refuses to build a program where the types don't line up.

**Why people get this wrong.** Coming from Python or JavaScript, you might expect a variable to be a box
you can drop anything into. In Java it's a box with a *declared shape* - number-shaped, text-shaped - and
the compiler checks everything fits. A whole category of bugs ("I thought this held a number but it held
`"42"`") can't survive to runtime; they're caught while you build.

📝 **Static typing** - the type of every variable is known and checked at *compile time*, not while the
program runs. "Static" is the opposite of "dynamic," where types are checked as code executes. Java checks
early, so a running program never wonders what type something is.

You declare a variable by writing its type, then its name, then optionally an initial value:

```java
int count = 0;
count = count + 5;   // fine: an int going into an int box
count = "hello";     // compile error: incompatible types
```

*What just happened:* `int count = 0;` declares `count` as type `int`, starting at `0`. The second line is
fine - a number into a number box. The third never compiles: the compiler sees text going into a
whole-number box and stops you cold with `incompatible types: String cannot be converted to int`. The
safety net doing its job at build time instead of letting a confused value blow up later.

## Primitives vs objects - the split that explains everything

This is the most important distinction in the phase.

📝 **Primitive** - a value that holds its data *directly*: the actual number or true/false sits right in
the variable. **Object** (also called a *reference type*) - a value that lives elsewhere in memory; the
variable holds a *reference* (a pointer-like handle) to it, not the thing itself.

Java has exactly **eight** primitive types, the only types that work this way. The ones you'll actually
reach for:

- **`int`** - a whole number (`-3`, `0`, `42`). Default for counting.
- **`double`** - a number with a decimal point (`3.14`, `-0.5`). Default for fractional numbers.
- **`boolean`** - a truth value, `true` or `false`. Named after George Boole.
- **`char`** - a single character, written in *single* quotes (`'A'`, `'7'`).
- **`long`** - a whole number with extra room, for values too big for `int` (literal suffix `L`:
  `9_000_000_000L`).

(The other three - `byte`, `short`, `float` - exist for specific low-level needs; ignore them while
learning.)

**Everything that isn't one of those eight is an object.** `String`, arrays, `Scanner`, and every class you
write are reference types, holding a reference to data that lives elsewhere.

**Why this distinction earns its keep.** Two reasons:

1. **Performance.** A primitive is tiny and lives right where it's declared - no indirection, no extra
   allocation. Objects carry more overhead.
2. **`null`.** A reference can point at *nothing* - the special value `null`. A primitive *cannot*; an
   `int` is always some number, never "missing." That's the root of Java's most famous crash, the
   `NullPointerException`, properly met in [Phase 9](09-idioms-and-gotchas.md). For now: primitives can't
   be `null`; objects can.

💡 **Mental model.** A primitive variable *is* the value. An object variable is a *label on a box stored
elsewhere*, and that label can point at nothing. When Java surprises you, ask "primitive or object?" first.

## Wrapper types & autoboxing - the object versions of primitives

Sometimes a primitive needs to behave like an object - most commonly because a collection (a list, a map)
can only hold objects, never raw primitives. For that, every primitive has an **object twin** called a
**wrapper type**: `int` ↔ `Integer`, `double` ↔ `Double`, `boolean` ↔ `Boolean`, `char` ↔ `Character`.

📝 **Wrapper type** - an object holding a single primitive value. `Integer` wraps an `int`; `Double` wraps
a `double`. Being an object, a wrapper can be stored where only objects are allowed - and, unlike its
primitive, it can be `null`.

The convenient part is **autoboxing**: Java converts between a primitive and its wrapper automatically, so
you rarely write the conversion yourself.

```java
Integer boxed = 42;     // autoboxing: int 42 wrapped into an Integer
int unboxed = boxed;    // auto-unboxing: Integer back to int
```

*What just happened:* `Integer boxed = 42;` assigns an `int` to an `Integer` - Java silently *boxed* the
primitive `42` into an object. The reverse line *unboxed* it back to a raw `int`; the compiler inserts the
conversion for you. That convenience hides two traps.

⚠️ **A wrapper can be `null`, so unboxing can crash.** Because `Integer` is an object, it can be `null` -
and auto-unboxing a `null` wrapper doesn't give you `0`, it throws a `NullPointerException`.

```java
Integer maybe = null;   // perfectly legal - it's an object
int n = maybe;          // NullPointerException at runtime when unboxing null
```

*What just happened:* `maybe` is an `Integer`, so `null` is valid for it. The next line unboxes it into a
plain `int` with no number to hand over - Java throws a `NullPointerException`. A plain `int` could never
have gotten you here; it's the `null` risk from the previous section, made concrete.

⚠️ **`==` on wrappers compares references, not values.** This bites everyone exactly once. On primitives,
`==` compares the actual numbers; on *objects* (wrappers included), it asks "are these the same object in
memory?" - a different question, often with a surprising answer.

```java
Integer a = 1000;
Integer b = 1000;
boolean sameValue = (a == b);            // false! comparing two distinct objects
boolean reallySame = a.equals(b);        // true - equals() compares the values
```

*What just happened:* `a` and `b` both wrap `1000` but are two *separate* `Integer` objects, so `a == b`
asks "same object?" and gets `false`. `.equals()` checks the numbers inside instead. (Java caches small
`Integer`s from -128 to 127, so the same test with `127` would confusingly print `true` - why you should
never rely on `==` for wrappers.) More in [Phase 9](09-idioms-and-gotchas.md); rule for now: **use
`.equals()` to compare objects, save `==` for primitives.**

## `var` - let the compiler infer the type

Writing the type twice - `ArrayList<String> names = new ArrayList<String>();` - gets tedious when it's
already obvious from the right side. Modern Java (10+) lets you write **`var`** instead; the compiler
*infers* the type from the assigned value.

```java
var name = "Ada";        // compiler infers: String
var count = 0;           // compiler infers: int
var price = 9.99;        // compiler infers: double
```

*What just happened:* `var name = "Ada";` declares `name` and lets the compiler see `"Ada"` is text and
decide `name` is a `String`. The variable is *exactly* as statically typed as if you'd written `String
name` - `var` is shorthand, not a change in how typing works. Try `name = 5;` afterward and you still get a
compile error, because `name`'s type locked to `String` the moment it was inferred.

💡 **When to reach for `var`.** Use it when the type is plainly visible on the right side, especially to
avoid repeating a long type. Two limits: it only works for **local variables** inside methods (not fields or
parameters), and it needs an initial value to infer *from* - `var x;` alone won't compile. When the right
side is vague (`var result = compute();`), spell the type out for readability.

## Strings - objects, immutable, and never compared with `==`

You've used `String` since Phase 1, but a few facts about it will save you real grief.

**A `String` is an object, not a primitive.** Despite friendly syntax (double quotes, `+`), `String` is a
reference type - everything from the objects section applies to it.

**A `String` is immutable.** Once created, its characters never change - operations that look like they
modify a string actually build and return a *new* one, leaving the original untouched. Concatenate with
`+`:

```java
String first = "Ada";
String full = first + " " + "Lovelace";   // builds a brand-new String
System.out.println(full);
System.out.println(first);                // unchanged
```
```console
$ java Strings.java
Ada Lovelace
Ada
```

*What just happened:* `first + " " + "Lovelace"` didn't alter `first`; it assembled a *new* `String` and
stored it in `full`. `first` is still just `"Ada"` - strings can't be mutated in place. Every "string
change" in Java is really "make a new string."

⚠️ **Never compare strings with `==`.** Since `String` is an object, `==` compares *references*, not text.
Two strings with identical characters can live at different memory addresses, so `==` may say `false` even
when they read the same. Always use `.equals()` for *contents*:

```java
String a = "hello";
String b = new String("hello");   // forces a distinct object
System.out.println(a == b);        // false - different objects
System.out.println(a.equals(b));   // true  - same characters
```
```console
$ java Compare.java
false
true
```

*What just happened:* `a` and `b` hold the same text but are two separate objects (`new String(...)`
guarantees a fresh one). `a == b` compares identity, `false`; `a.equals(b)` compares characters, `true`.
The single most common beginner trap in Java: **`.equals()` for "same thing?", never `==`.** Same rule as
wrappers, same cause - both are objects.

**Text blocks for multi-line strings.** For a string spanning several lines, the triple-quote **text
block** (Java 15+) saves you from a mess of `\n` escapes:

```java
String message = """
    Dear Ada,
    Welcome to Java.
    """;
System.out.print(message);
```
```console
$ java Block.java
Dear Ada,
Welcome to Java.
```

*What just happened:* The `"""..."""` block let you write the message exactly as it should appear, no `\n`
escapes needed. Java strips the common leading indentation (measured from the closing `"""`), so text lines
up cleanly in source *and* output. Same immutable `String` type - just a more readable way to write a long
one.

## Recap

1. Java is **statically typed**: every variable has a declared type the compiler checks at build time, so
   type mistakes are caught before the program runs.
2. The big split is **primitives vs objects**. The eight primitives (`int`, `double`, `boolean`, `char`,
   `long`, …) hold their value directly and are cheap; everything else is an **object** reached by
   reference - and only objects can be `null`.
3. **Wrapper types** (`Integer`, `Double`, …) are the object twins of primitives; **autoboxing** converts
   between them automatically. ⚠️ A wrapper can be `null` (unboxing it crashes), and `==` on wrappers
   compares references, not values - use `.equals()`.
4. **`var`** lets the compiler infer a local variable's type from its initializer. It's still fully static
   - just less typing - and works only for locals with an initial value.
5. **`String` is an immutable object.** Concatenate with `+` (it builds a new string); write multi-line
   text with `"""` text blocks.
6. ⚠️ **Never compare strings (or any objects) with `==`** - that checks identity. Use `.equals()` to
   compare contents.

Next: *collections* of values - arrays, the `List` and `Map` you'll reach for daily, and how generics keep
them type-safe.

## Quick check

Test yourself on the idea driving this phase - which side of the primitive/object line you're on:

```quiz
[
  {
    "q": "What's the key difference between a primitive like `int` and an object like `Integer`?",
    "choices": [
      "An `int` holds its value directly and can never be null; an `Integer` is an object reference that can be null",
      "There is no difference - `int` and `Integer` are two names for the same thing",
      "An `Integer` is faster because it's stored directly in the variable",
      "A primitive can be null but an object cannot"
    ],
    "answer": 0,
    "explain": "A primitive holds its data directly and is always some value - an `int` is never \"missing.\" `Integer` is a wrapper object reached by reference, so it can be `null`, which is exactly why unboxing a null `Integer` throws a NullPointerException."
  },
  {
    "q": "You have two `String` variables holding the same text. How should you check whether their contents match?",
    "choices": [
      "Use `a.equals(b)`, because `==` compares object references, not the characters",
      "Use `a == b`, which always compares the text of two strings",
      "Either works - `==` and `.equals()` do the same thing for strings",
      "Convert both to `int` first, then compare with `==`"
    ],
    "answer": 0,
    "explain": "`String` is an object, so `==` asks \"is this the same object in memory?\" - which can be false even when the text is identical. `.equals()` compares the actual characters, which is what you almost always want."
  },
  {
    "q": "What does `var name = \"Ada\";` do?",
    "choices": [
      "Declares a local variable whose type the compiler infers as `String` - still fully static, just less typing",
      "Declares a variable with no type, so it can later hold any kind of value",
      "Makes `name` dynamically typed, like a Python variable",
      "Only works for fields, not for local variables inside methods"
    ],
    "answer": 0,
    "explain": "`var` infers the type from the initializer - here `String` - and locks it in. It's pure shorthand: the variable is exactly as statically typed as if you'd written `String name`, and it works only for local variables that have an initial value."
  }
]
```


---

# Collections - Arrays, Lists, Maps & Sets

Up to now you've held one value at a time. Real programs deal in *many*: a roster of users, a table of
prices, the unique tags on a post. Java gives you two layers for this - the old, low-level **array**, and
the rich **Collections Framework** on top of it. Newcomers waste energy fighting arrays when they should
reach for a `List`. We'll meet the array, see when it earns its keep, then spend the rest of the phase on
the collections you'll actually use daily.

The mental model: an **array** is a fixed slab of memory you size once and live with. A **collection** is a
smart, growable object that manages that slab for you, letting you ask by *behavior* ("ordered list," "fast
lookup by key") instead of wrestling the memory yourself.

## Arrays - the fixed-size foundation

**What it actually is.** An **array** is a fixed-length sequence of values, all the same type, laid out
back-to-back in memory. You choose the length at creation; it never changes. `int[]` reads as "an array of
ints."

```java
int[] nums = {1, 2, 3};
System.out.println(nums[0]);      // first element
System.out.println(nums[2]);      // third element
System.out.println(nums.length);  // how many slots
```
```console
1
3
3
```
*What just happened:* `{1, 2, 3}` created an array with three slots, already filled. `nums[0]` reads the
**first** element (Java counts from zero), `nums[2]` the third. `nums.length` tells you the size - a
**field, no parentheses**. Reaching past the end (`nums[3]`) throws an `ArrayIndexOutOfBoundsException` -
there's no fourth slot.

The catch is right there in "fixed-length": no `add`. Need more room? Allocate a bigger array and copy
everything by hand - exactly the chore the Collections Framework does for you.

💡 **When you'd reach for an array.** When the size is known and fixed (the 12 months, an RGB triple), when
you want the leanest memory for primitives, or when an API hands you one. For nearly everything else - it
grows, shrinks, or you're unsure of the size - reach for a `List`, next up and where most of your
collection work lives.

## The Collections Framework - the mental model

The idea that unlocks the rest of Java's collections: the framework splits into two halves.

📝 **Interfaces vs. implementations.** An **interface** describes *behavior* - what a collection can do,
not how. `List`, `Map`, and `Set` are interfaces. A **concrete class** provides the how; `ArrayList`,
`HashMap`, and `HashSet` are the workhorses. The interface is the *contract*; the class is the *machine*
that fulfills it.

Three interfaces cover almost everything you'll need:

- **`List`** - an *ordered* sequence accessed by position. Duplicates allowed. (Think: a numbered to-do list.)
- **`Map`** - a table of **key → value** pairs looked up by key. (Think: a phone book.)
- **`Set`** - an *unordered* bag of **unique** elements. (Think: the distinct words in a document.)

💡 **Program to the interface.** Strong Java convention: *type your variable as the interface* but *create
the concrete class*:

```java
List<String> names = new ArrayList<>();
```

The variable is a `List`; the object is an `ArrayList` doing the work. The rest of your code only depends on
"it's a list," so the day a `LinkedList` suits better, you change one line - the `new` - and nothing else
breaks. Coding against the *promise*, not the *machinery*.

The `<String>` part is **generics**: it tells Java "this list holds `String`s and nothing else" - the
feature that makes collections type-safe.

## `List` and `ArrayList` - the growable sequence

**What it actually is.** A `List` is an ordered, indexed sequence - like an array, but growing and shrinking
on demand. `ArrayList` is the implementation you'll use 95% of the time; under the hood it manages a real
array, invisibly reallocating and copying when it fills up.

```java
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("Ada");
        names.add("Alan");
        names.add("Grace");

        System.out.println(names.get(0));   // read by index
        System.out.println(names.size());   // how many

        for (String name : names) {         // the for-each loop
            System.out.println(name);
        }
    }
}
```
```console
Ada
3
Ada
Alan
Grace
```
*What just happened:* We declared `names` as a `List<String>` but built an `ArrayList<>()` - programming to
the interface, exactly as above. (The empty `<>` is the "diamond"; Java infers `String` from the left side.)
`add` appended each name to the end. `get(0)` read the first element by index; `size()` reported the count
*with* parentheses, unlike an array's `length` field. The **for-each** loop (`for (String name : names)`)
visited every element in order, no index needed - the cleanest way to walk a collection.

The generics payoff shows up the moment you slip: because `names` is a `List<String>`, this won't compile:

```java
names.add(42);   // compile error: int is not a String
```
*What just happened:* The `<String>` is a promise the **compiler enforces**. Adding an `int` is caught
before the program ever runs, not as a surprise crash later - type mistakes become compile-time errors
instead of `ClassCastException`s in production.

## `Map` and `HashMap` - lookup by key

A `List` suits *order* and *position*. But often you want to look something up by *name* - a user's score
by username, a setting's value by its key. That's a `Map`.

**What it actually is.** A `Map` stores **key → value** pairs and fetches a value instantly by key.
`HashMap` is the standard implementation. `Map<String, Integer>` reads as "a map from `String` keys to
`Integer` values." (Other languages call this a dictionary, hash, or associative array.)

```java
import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> ages = new HashMap<>();
        ages.put("Ada", 36);
        ages.put("Alan", 41);

        System.out.println(ages.get("Ada"));               // look up by key
        System.out.println(ages.getOrDefault("Nobody", 0)); // safe default

        for (Map.Entry<String, Integer> entry : ages.entrySet()) {
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }
    }
}
```
```console
36
0
Alan -> 41
Ada -> 36
```
*What just happened:* `put` stored two key→value pairs. `get("Ada")` returned `36`. The interesting one is
`getOrDefault("Nobody", 0)`: plain `get` on a missing key returns `null` (often a `NullPointerException` two
lines later), so `getOrDefault` says "give me the value, or this fallback" - here, `0`. To visit every pair,
we looped over `entrySet()`, which hands back each pair as a `Map.Entry` with `getKey()`/`getValue()` - the
standard way to iterate a map.

⚠️ **Gotcha - `get` returns `null` for a missing key.** It does *not* throw. Use the result blindly and a
missing key turns into a `NullPointerException` downstream, far from the real cause. Reach for
`getOrDefault` (or check `containsKey` first) whenever a key might be absent.

## `Set` and `HashSet` - uniqueness, fast

**What it actually is.** A `Set` holds **unique** elements - add the same value twice and the second add is
silently ignored. `HashSet` is the standard implementation, answering "is this in here?" almost instantly.

```java
import java.util.HashSet;
import java.util.Set;

public class Main {
    public static void main(String[] args) {
        Set<String> tags = new HashSet<>();
        tags.add("java");
        tags.add("beginner");
        tags.add("java");          // duplicate - ignored

        System.out.println(tags.size());            // 2, not 3
        System.out.println(tags.contains("java"));  // fast membership test
    }
}
```
```console
2
true
```
*What just happened:* We added `"java"` twice, but the set kept only one copy, so `size()` is `2`.
`contains` checked membership and returned `true` immediately - `HashSet` is built for exactly that "have I
seen this before?" question. Use a `Set` whenever uniqueness is the point: de-duplicating a list, tracking
processed items, testing membership in a hot loop.

💡 **Picking the right one.** Let the question pick the collection. **Order and position** (or duplicates)?
→ `List`. **Lookup by key**? → `Map`. **Uniqueness or fast membership**? → `Set`.

⚠️ **Gotcha - `HashMap` and `HashSet` have no order.** They're optimized for speed, not for preserving
insertion order - iterate one and the order can look scrambled, with no tie to how you inserted. If you need order,
the framework has drop-in replacements: **`LinkedHashMap`/`LinkedHashSet`** preserve *insertion* order, and
**`TreeMap`/`TreeSet`** keep keys *sorted*. Same interfaces, so swapping one in is a one-line change.

## Recap

1. An **array** (`int[]`) is fixed-size and lean - great when the count never changes, awkward when it does.
   `.length` is a field, not a method.
2. The **Collections Framework** splits into **interfaces** (`List`, `Map`, `Set` - behavior) and **concrete
   classes** (`ArrayList`, `HashMap`, `HashSet` - implementation).
3. **Program to the interface:** `List<String> names = new ArrayList<>();`. Your code depends on the
   contract, so swapping implementations costs one line.
4. **`List`/`ArrayList`** is the growable ordered sequence: `add`, `get`, `size`, and the **for-each** loop.
   **Generics** (`<String>`) make it type-safe at compile time.
5. **`Map`/`HashMap`** does key→value lookup: `put`, `get`, `getOrDefault`, and `entrySet()` to iterate -
   but `get` returns `null` for a missing key.
6. **`Set`/`HashSet`** holds unique elements with fast `contains`. ⚠️ `HashMap`/`HashSet` are **unordered**;
   use `LinkedHashMap`/`TreeMap` (and the `Set` equivalents) when order matters.

Next: making programs *decide* and *organize logic* - `if`/`switch`, loops, and the methods that give Java
code its shape.

## Quick check

Test yourself on the habit that makes Java collections click - choosing by behavior and coding to the
interface:

```quiz
[
  {
    "q": "Why is `List<String> names = new ArrayList<>();` preferred over `ArrayList<String> names = new ArrayList<>();`?",
    "choices": [
      "The variable is typed to the List interface, so your code depends on behavior and you can swap the implementation with a one-line change",
      "It runs faster because List is a smaller type than ArrayList",
      "ArrayList cannot be assigned to a variable at all",
      "It automatically makes the list unmodifiable"
    ],
    "answer": 0,
    "explain": "Programming to the interface means the rest of your code only knows it has a `List`. The concrete class lives in one place - the `new` - so switching to, say, a `LinkedList` changes that single line and nothing else."
  },
  {
    "q": "You call `ages.get(\"Nobody\")` on a `HashMap<String, Integer>` that has no \"Nobody\" key. What happens?",
    "choices": [
      "It returns `null` (which can cause a NullPointerException downstream) - use `getOrDefault` to avoid this",
      "It throws a KeyNotFoundException immediately",
      "It returns 0 because the value type is Integer",
      "It adds the key with a null value and returns it"
    ],
    "answer": 0,
    "explain": "`get` returns `null` for a missing key rather than throwing. That null often blows up later, far from the cause. `getOrDefault(\"Nobody\", 0)` hands back a safe fallback instead."
  },
  {
    "q": "You need to store the distinct tags on a post and quickly check whether a given tag is already present. Which collection fits best?",
    "choices": [
      "A Set (HashSet) - it keeps elements unique and answers `contains` fast",
      "A List (ArrayList) - it preserves insertion order",
      "A Map (HashMap) - it stores key→value pairs",
      "An array - it has a fixed size"
    ],
    "answer": 0,
    "explain": "Uniqueness plus fast membership is exactly what a `Set` is for. A `HashSet` ignores duplicate adds and answers `contains` almost instantly - the right tool when 'is this already in here?' is the core question."
  }
]
```


---

# Control Flow & Methods - Decisions, Loops & Reusable Logic

So far your programs have run in a straight line: top to bottom, every statement once. Real programs make
decisions ("if logged in, show the dashboard"), repeat work ("for every order, send a receipt"), and bundle
logic into named pieces you can call again and again. Branching, looping, and methods are the joints that
let a program bend.

The mental model for this phase: **control flow is about choosing which statements run and how often, and
methods are about naming a chunk of statements so you can reuse it.** Everything below is a variation on
those two themes.

## `if` / `else` - making a decision

The most basic branch: give Java a **boolean expression** - something evaluating to `true` or `false` - and
it picks a path.

```java
int temperature = 30;

if (temperature > 25) {
    System.out.println("Warm");
} else if (temperature > 10) {
    System.out.println("Mild");
} else {
    System.out.println("Cold");
}
```
```console
Warm
```
*What just happened:* Java checked the conditions top to bottom and ran the **first** block whose
condition was `true`. `temperature > 25` is `30 > 25`, `true`, so it printed `Warm` and skipped the rest
entirely - once a branch wins, the others aren't even evaluated. Parentheses around the condition are
required in Java (unlike Python), and braces `{ }` group each branch's statements.

Conditions are boolean expressions, built with comparison operators (`>`, `<`, `>=`, `<=`, `==`, `!=`) and
combined with `&&` (and), `||` (or), and `!` (not):

```java
boolean loggedIn = true;
int age = 20;

if (loggedIn && age >= 18) {
    System.out.println("Access granted");
}
```
```console
Access granted
```
*What just happened:* `loggedIn && age >= 18` is `true && (20 >= 18)`, i.e. `true && true`, so the block
ran. `&&` is **short-circuiting**: if the left side were `false`, Java wouldn't check the right side at all,
since the whole thing can't be `true` anymore. That's not just a speed trick - it lets you write guards like
`if (user != null && user.isActive())` where the second check is only safe *because* the first passed.

💡 **Key point.** Keep conditions explicit and readable. `if (count > 0)` says exactly what it means.
Resist nested ternaries or stacked negations - code is read far more than it's written, and a clear `if` is
a gift to whoever debugs this at 2 a.m.

## `switch` - one value, many cases

When checking a *single* value against many possible matches, a tall stack of `else if` gets noisy.
`switch` is built for that. Java has two forms, and the difference matters.

### The classic `switch` statement

The old form has a sharp edge worth knowing. Here it is, done correctly:

```java
int day = 3;
String name;

switch (day) {
    case 1:
        name = "Monday";
        break;
    case 2:
        name = "Tuesday";
        break;
    case 3:
        name = "Wednesday";
        break;
    default:
        name = "Unknown";
}

System.out.println(name);
```
```console
Wednesday
```
*What just happened:* `switch (day)` jumped to the `case` matching `day` (`3`), set `name` to `"Wednesday"`,
and `break` stopped it. `default` is the catch-all when nothing matches. Burn that `break` into memory.

⚠️ **Gotcha - fall-through.** In the classic `switch`, execution does **not** stop at the end of a `case`.
It runs straight into the next `case` until it hits a `break` (or the end of the switch). Forget a `break`
and you get a bug that's hard to spot:

```java
int day = 1;
switch (day) {
    case 1:
        System.out.println("Monday");
        // no break - execution falls through!
    case 2:
        System.out.println("Tuesday");
        break;
}
```
```console
Monday
Tuesday
```
*What just happened:* `day` was `1`, so it matched `case 1` and printed `"Monday"`. No `break`, so
execution **fell through** into `case 2` and printed `"Tuesday"` too - even though `day` isn't `2`. The
single most common `switch` mistake, and exactly why the modern form exists. (Fall-through is occasionally
useful when several cases *should* share code, but that's a deliberate, documented choice - never an
accident.)

### The modern `switch` expression

Newer Java (14+) gives you a `switch` *expression* with arrow syntax: it returns a value, never falls
through, and reads cleanly.

```java
int day = 3;

String name = switch (day) {
    case 1 -> "Monday";
    case 2 -> "Tuesday";
    case 3 -> "Wednesday";
    default -> "Unknown";
};

System.out.println(name);
```
```console
Wednesday
```
*What just happened:* The whole `switch (...) { ... }` *evaluated to a value* assigned straight into `name`
- notice the `=` before it and `;` after the closing brace. Each `case ... ->` runs only its own branch
with no fall-through, so there's no `break` to forget. You can list several values in one case
(`case 1, 2, 3 -> ...`), and the compiler can even warn you if you miss a possible value. Same idea as the
classic form, minus the footgun.

💡 **Key point.** Reach for the **switch expression** (`->`) by default in modern Java: safer (no
fall-through), more concise, and it produces a value you can assign or return directly. Keep the classic
form only for older code or genuine fall-through needs.

## Loops - repeating work

Three loop shapes cover almost everything, differing in *when* you know how many times to repeat.

**`for`** - when you're counting, or you know the iteration count up front:

```java
for (int i = 0; i < 3; i++) {
    System.out.println("Pass " + i);
}
```
```console
Pass 0
Pass 1
Pass 2
```
*What just happened:* The `for` header has three parts: **init** (`int i = 0`, runs once), **condition**
(`i < 3`, checked before every pass), and **update** (`i++`, adds one to `i` after each pass). `i` walked
through `0`, `1`, `2`, and the loop stopped the moment `i` reached `3`.

**`while`** - when you repeat until some condition flips, and you don't know the count ahead of time:

```java
int countdown = 3;
while (countdown > 0) {
    System.out.println(countdown);
    countdown--;
}
```
```console
3
2
1
```
*What just happened:* `while (countdown > 0)` checked the condition before each pass and ran the body as
long as it held. Each pass printed `countdown`, then `countdown--` subtracted one. When `countdown` hit `0`,
the condition became `false` and the loop ended. ⚠️ Forget the `countdown--` and the condition never
changes - an **infinite loop**, the classic "why is my program frozen?" bug.

**Enhanced `for` (for-each)** - to visit every element of a collection or array without caring about index
numbers:

```java
List<String> names = List.of("Ada", "Linus", "Grace");

for (String name : names) {
    System.out.println(name);
}
```
```console
Ada
Linus
Grace
```
*What just happened:* `for (String name : names)` reads as "for each `name` in `names`." Each pass binds
`name` to the next element, in order, until the list runs out - no index, no `i++`, no off-by-one risk.
Prefer it whenever you don't need the index. (`List.of(...)` came from [Phase 3](03-collections.md).)

💡 **Key point.** Pick the loop that says what you mean: **for-each** to walk a collection, **`for`** to
count or need the index, **`while`** to loop until a condition changes.

## Methods - naming reusable logic

Once you've written the same handful of statements twice, it's time for a method.

📝 **Method** - a named, reusable block of logic with a **return type** (what it hands back, or `void` for
nothing), a **name**, a list of **parameters** (its inputs), and a body. An **access modifier** like
`public` or `private` controls who's allowed to call it. You "call" a method by its name to run its body.

```java
public class Calculator {

    // a method: returns an int, named add, takes two int parameters
    public static int add(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        int sum = add(3, 4);
        System.out.println(sum);
    }
}
```
```console
7
```
*What just happened:* `public static int add(int a, int b)` declares a method named `add` taking two `int`
parameters and **returning** an `int`. Inside `main`, `add(3, 4)` called it with `3` and `4`, which became
`a` and `b`; `return a + b` handed back `7`, stored in `sum`. The return type comes *first* in Java
(`int add(...)`), unlike Go where it comes last.

You've been staring at one keyword on every method so far: **`static`**. Just enough to read it.

📝 **`static` vs instance.** A `static` method belongs to the **class itself** - call it without creating an
object (`Calculator.add(3, 4)`). A non-static (**instance**) method belongs to an individual **object**. We
lean on `static` heavily now since we haven't built objects yet - that's all of
[Phase 5](05-classes-and-objects.md). For now, `static` is why `main` can run before any object exists.

💡 **Key point.** A good method does **one thing** and has a name that says what that thing is. Struggling
to name it often signals it's doing too much - split it.

## Method overloading - same name, different parameters

Sometimes you want one logical operation that works on different inputs. Java lets you give several methods
the **same name** as long as their **parameter lists differ** - different types, or a different count. This
is **overloading**.

📝 **Overloading** - defining multiple methods with the same name but distinct parameter lists. The
compiler decides which one to call by looking at the *types and number of arguments* at the call site.

```java
public class Printer {

    public static void show(int x) {
        System.out.println("int: " + x);
    }

    public static void show(String x) {
        System.out.println("String: " + x);
    }

    public static void show(int x, int y) {
        System.out.println("two ints: " + x + ", " + y);
    }

    public static void main(String[] args) {
        show(42);
        show("hello");
        show(1, 2);
    }
}
```
```console
int: 42
String: hello
two ints: 1, 2
```
*What just happened:* All three methods are named `show`, but each takes a different parameter list.
`show(42)` matched `show(int x)`; `show("hello")` matched the `String` version; `show(1, 2)` matched the
two-parameter one. One name, three behaviors, chosen by what you pass in - no need for `showInt`,
`showString`, `showTwoInts`.

💡 **Resolved at compile time.** The compiler picks the overload by inspecting argument types *while it
compiles*, baked in before your code runs - a real distinction from the next concept.

⚠️ **Don't confuse overloading with overriding.** They sound alike but are opposites. **Overloading** is
*same name, different parameters, picked at compile time*. **Overriding** is when a subclass *replaces* an
inherited method (same name, same parameters), decided *at runtime* by the actual object - needs
inheritance, met in [Phase 6](06-inheritance-and-interfaces.md).

## Recap

1. **`if` / `else`** branches on a **boolean expression** and runs the first matching block; build
   conditions with `>`, `==`, `&&`, `||`, `!`, and lean on short-circuiting for safe guards.
2. **`switch`** matches one value against many cases. ⚠️ The classic statement **falls through** without
   `break`; prefer the modern **switch expression** (`->`), which returns a value and never falls through.
3. **Loops** come in three shapes: **`for`** for counting, **`while`** for looping until a condition flips,
   and the **enhanced for-each** for walking a collection without an index.
4. A **method** packages reusable logic with a **return type**, a name, **parameters**, and an access
   modifier; **`static`** methods belong to the class (no object needed), which is why `main` is static.
5. **Overloading** gives one name several parameter lists, and the **compiler** picks the right one by
   argument types - distinct from **overriding** (a runtime, inheritance concept coming in Phase 6).

Next: we stop writing everything as `static` helpers and build the real thing Java is named for -
**classes and objects**, your own types with their own data and behavior.

## Quick check

Test yourself on the ideas most likely to bite - fall-through, loop choice, and overloading:

```quiz
[
  {
    "q": "In a classic `switch` statement, what happens if a matching `case` has no `break`?",
    "choices": [
      "Execution falls through and runs the following case(s) until it hits a break or the end",
      "Java throws a compile error demanding a break",
      "Only that case runs, then the switch ends automatically",
      "The default case runs instead"
    ],
    "answer": 0,
    "explain": "Without `break`, execution falls through into the next case and keeps going. This is the most common switch bug, and exactly why the modern switch expression (with `->` arrows) never falls through."
  },
  {
    "q": "You want to visit every element of a `List<String>` and don't need the index. Which loop fits best?",
    "choices": [
      "The enhanced for-each: `for (String s : list)`",
      "A `while` loop with a manual counter",
      "A classic `for` loop with `i++`",
      "A `switch` statement"
    ],
    "answer": 0,
    "explain": "The for-each loop binds each element in turn with no index, no `i++`, and no off-by-one risk. Use a classic `for` only when you actually need the index."
  },
  {
    "q": "Given `show(int x)` and `show(String x)`, how does Java decide which `show` runs when you call `show(42)`?",
    "choices": [
      "The compiler picks the `int` version based on the argument type, at compile time",
      "The JVM picks one at random at runtime",
      "It always calls the first method declared",
      "It calls both versions in order"
    ],
    "answer": 0,
    "explain": "This is overloading: the compiler resolves which overload to call by inspecting the argument types while it compiles. `42` is an int, so `show(int x)` is chosen - the decision is made before the program runs."
  }
]
```


---

# Classes & Objects - Java's Whole Worldview

Up to now you've written methods, loops, and `if` statements inside something called `class Main` that you
mostly ignored. This phase is where `class` stops being scenery and becomes the point. In Java, the class
isn't *a* feature - it's *the* feature. Almost everything you build is a class, and almost every value you
touch is an object made from one.

That worldview runs deeper than in most languages. Python lets you write a loose function in a file; Go
has standalone functions everywhere. Java does not - no code lives outside a class. This phase shows you
how to think *in* classes: bundle data with its behavior, hand out copies of that bundle, protect it from
the rest of your program.

## The mental model: blueprint and instance

**What it actually is.** A **class** is a blueprint - a description that says "things of this kind hold
*this* data and can do *these* things." An **object** (also called an **instance**) is one concrete thing
built from it with the `new` keyword. The class is the architect's drawing; objects are the actual houses
built from it - one drawing, as many houses as you like, each with its own address and furniture.

📝 **Class** - the template/blueprint, written once. **Object / instance** - one real thing made from it,
created with `new`. The class `Account` is the idea of a bank account; `new Account(...)` is *your*
account, separate from everyone else's.

**Why this is the whole worldview.** In many languages, classes are one tool among several. In Java, every
program is a set of classes, every value with behavior is an object, and even `main` sits inside one. Stop
asking "where do I put this loose code?" and start asking "what *kind of thing* is this?"

> 💡 **Key point.** Everything below is one sentence repeated in different clothes: *the data and the
> behavior that belongs with it live together inside an object.* When a detail feels arbitrary, return to
> that line.

## Fields, constructors, and `this`

Let's build a real blueprint. An `Account` holds data (an owner's name, a balance) and offers behavior
(deposit, check balance). Data lives in **fields**; setup happens in a **constructor**; `this` is how a
method points at *its own* object.

**What it actually is.** A **field** is a variable belonging to each object - its own slice of data. A
**constructor** is a special method, named exactly like the class, that runs once when you write `new`, to
fill in the object's starting fields. **`this`** refers to "the object this method is running on" - reach
for it when a parameter name collides with a field name.

```java
public class Account {
    private String owner;     // a field - each Account gets its own
    private double balance;   // another field

    public Account(String owner, double balance) {  // constructor: same name as the class
        this.owner = owner;       // this.owner = the field; owner = the parameter
        this.balance = balance;
    }

    public void deposit(double amount) {
        this.balance += amount;   // 'this.' is optional here - no name clash
    }

    public double getBalance() {
        return balance;           // reading the field directly
    }
}
```
```java
public class Main {
    public static void main(String[] args) {
        Account ada = new Account("Ada", 100.0);  // build one instance
        Account bob = new Account("Bob", 50.0);    // build another, totally separate

        ada.deposit(25.0);

        System.out.println(ada.getBalance());
        System.out.println(bob.getBalance());
    }
}
```
```console
$ java Main.java
125.0
50.0
```
*What just happened:* `new Account("Ada", 100.0)` allocated a fresh object and ran the constructor, copying
the parameters into that object's own `owner` and `balance` fields. `ada` and `bob` are independent
objects: depositing into `ada` changed `ada`'s balance and left `bob`'s untouched, since each carries its
own copy of the data - that separateness is the entire reason objects exist.

📝 **`this`** - a reference to the current object. Inside the constructor, `owner` (parameter) and
`this.owner` (field) share a name; `this.owner = owner` means "store the parameter into my field." Without
`this.`, you'd assign the parameter to itself and the field stays empty. ⚠️ A classic silent bug - write
`owner = owner` and your account starts up with a blank name and no error to explain why.

## Instance vs static: the object vs the class itself

Some things belong to *each object*; some belong to *the class as a whole*. Java draws that line with the
keyword `static`, explaining a mystery from Phase 1: why `main` is `static`.

**What it actually is.** An **instance member** (no `static`) belongs to each object - every `Account` has
its own `balance`. A **static member** (`static`) belongs to the *class itself* - exactly one copy, shared
by everyone, existing even if you never create a single object.

```java
public class Account {
    private static int accountCount = 0;  // ONE counter, shared by the whole class
    private String owner;                  // each object's own field

    public Account(String owner) {
        this.owner = owner;
        accountCount++;                    // bump the shared counter on every new Account
    }

    public static int getAccountCount() {  // a static method - call it on the class
        return accountCount;
    }
}
```
```java
public class Main {
    public static void main(String[] args) {
        new Account("Ada");
        new Account("Bob");
        new Account("Cy");

        // Called on the CLASS, not on an object:
        System.out.println(Account.getAccountCount());
    }
}
```
```console
$ java Main.java
3
```
*What just happened:* `accountCount` is `static`, so it isn't stored inside any one `Account` - it lives on
the class, and all three constructors incremented the *same* counter. We read it with
`Account.getAccountCount()` (on the class), not `ada.getAccountCount()` (on an object), since it was never
about any single account. Instance data answers "what's true of *this* object?"; static data answers
"what's true of *all of them at once*?"

💡 **Why `main` is `static`.** When a program runs, no objects exist yet - the JVM hasn't created anything,
so the entry point can't be an instance method. `static` means "this belongs to the class and can run with
zero objects in existence" - exactly what a starting point needs.

## Encapsulation: expose behavior, not raw data

Every field above was marked `private`. That's not decoration - it's the single most important habit in
object-oriented Java.

📝 **Encapsulation** - keeping an object's data (`private` fields) hidden from the outside world, letting
other code interact with it *only* through the object's methods. Nobody reaches in and changes a field
directly.

**Why hiding state prevents whole classes of bugs.** If `balance` were `public`, *any* code could write
`ada.balance = -9999` and your account would silently go invalid - and finding that negative balance later,
you'd have no idea which of a hundred lines did it. Make the field `private` and force all changes through
one method, and that method becomes the *one* checkpoint every change must pass.

Here's a setter that refuses to let the balance go negative:

```java
public class Account {
    private double balance;

    public Account(double balance) {
        this.balance = balance;
    }

    public void withdraw(double amount) {
        if (amount > balance) {              // the guard lives in ONE place
            System.out.println("Denied: insufficient funds");
            return;                          // reject the bad change, leave balance untouched
        }
        balance -= amount;
    }

    public double getBalance() {
        return balance;                      // a getter: read access, no write access
    }
}
```
```java
public class Main {
    public static void main(String[] args) {
        Account ada = new Account(100.0);

        ada.withdraw(30.0);   // fine
        ada.withdraw(500.0);  // rejected by the guard

        System.out.println(ada.getBalance());
    }
}
```
```console
$ java Main.java
Denied: insufficient funds
70.0
```
*What just happened:* `balance` is `private`, so the only way to change it from outside is `withdraw`,
which checks the amount before touching the field. The bad withdrawal was rejected and the balance held at
`70.0`. With `withdraw` the only door that subtracts from `balance`, you get a guarantee: *no withdrawal can
ever push the balance negative*.

💡 **Expose behavior, not raw data.** Don't reflexively generate a getter and setter for every field - that
just makes the field public with extra steps. Ask what the object should let callers *do*: an account
should let you `deposit` and `withdraw`; whether it stores that as one `balance` field or a transaction
list is the object's private business.

## The trio: `toString`, `equals`, and `hashCode`

Two final pieces complete your mental model of a Java object - one is the single most common trap that
catches beginners. Both come down to: Java objects don't behave the way you'd hope until you *tell them
how*.

**Objects print as gibberish until you override `toString`.** Print an object you made and you'll get
something like `Account@1b6d3586` - class name and memory hash, useless to a human. Java calls `toString()`
whenever it needs a text version, and the default is that gibberish. Override it and printing makes sense:

```java
public class Account {
    private String owner;
    private double balance;

    public Account(String owner, double balance) {
        this.owner = owner;
        this.balance = balance;
    }

    @Override
    public String toString() {
        return owner + " ($" + balance + ")";
    }
}
```
```java
public class Main {
    public static void main(String[] args) {
        Account ada = new Account("Ada", 100.0);
        System.out.println(ada);   // println calls toString() for you
    }
}
```
```console
$ java Main.java
Ada ($100.0)
```
*What just happened:* `System.out.println(ada)` needed text, so it called `ada.toString()`. We overrode
that method to return a readable description, so instead of `Account@1b6d3586` we got `Ada ($100.0)`.
`@Override` tells the compiler "I mean to replace an inherited method" - misspell the name and it catches
the mistake instead of silently creating a new, uncalled method.

⚠️ **The #1 Java beginner trap: `==` vs `.equals()`.** This bites *everyone*. For objects, `==` does **not**
compare contents - it asks "are these the same object in memory?" To compare *value*, use `.equals()`. The
default `.equals()` you inherit *also* just checks identity - so until overridden, two accounts with
identical data count as unequal.

```java
public class Account {
    private String owner;
    private double balance;

    public Account(String owner, double balance) {
        this.owner = owner;
        this.balance = balance;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Account)) return false;
        Account other = (Account) o;
        return balance == other.balance && owner.equals(other.owner);
    }

    @Override
    public int hashCode() {
        return java.util.Objects.hash(owner, balance);  // override TOGETHER with equals
    }
}
```
```java
public class Main {
    public static void main(String[] args) {
        Account a = new Account("Ada", 100.0);
        Account b = new Account("Ada", 100.0);  // same data, different object

        System.out.println(a == b);        // same object in memory?
        System.out.println(a.equals(b));   // same value?
    }
}
```
```console
$ java Main.java
false
true
```
*What just happened:* `a` and `b` hold identical data but are two separate objects, so `a == b` is `false` -
`==` compares *identity*, not contents. Our overridden `.equals()` compares the actual fields, so
`a.equals(b)` is `true`. Rule to burn in: **use `.equals()` for value comparison, reserve `==` for "is it
literally the same object."** (Strings are the most common place this bites - Phase 9 has the full gotcha.)

⚠️ **Override `equals` and `hashCode` *together*, always.** Not optional politeness - it's a contract Java
relies on. Hash-based collections like `HashMap`/`HashSet` use `hashCode()` to find objects fast: *equal
objects must have equal hash codes.* Override `equals` but not `hashCode`, and two "equal" accounts can
land in different buckets - a `HashSet` stores both as distinct, a `HashMap` lookup fails to find a key
that's really there.

## Recap

1. A **class** is a blueprint; an **object / instance** is one thing built from it with **`new`**. In
   Java, this is the whole worldview - almost everything you build is a class.
2. **Fields** hold each object's own data; a **constructor** (same name as the class) fills them in when
   you call `new`; **`this`** points at the current object and disambiguates a field from a same-named
   parameter.
3. **Instance** members belong to each object; **`static`** members belong to the class itself (one shared
   copy) - which is exactly why `main` is `static`: it runs before any object exists.
4. **Encapsulation** means `private` fields plus methods as the only doors in. Putting the rules in one
   guarded method (reject a negative balance) prevents whole classes of bugs. Expose behavior, not raw data.
5. Override **`toString`** so objects print readably instead of as `Account@1b6d3586`.
6. ⚠️ `==` compares object *identity*; **`.equals()`** compares *value* - and you must override **`equals`
   and `hashCode` together** or hash-based collections break.

You can now design a Java object: bundle the data, guard it, give it behavior, make it print and compare
sensibly. Next: connecting objects to each other - how one class builds on another, and how interfaces let
unrelated classes promise the same behavior.

## Quick check

Test yourself on the ideas most likely to bite you in real code:

```quiz
[
  {
    "q": "Inside a constructor, why write `this.owner = owner` instead of just `owner = owner`?",
    "choices": [
      "`this.owner` is the object's field while `owner` is the parameter - without `this.`, you'd just assign the parameter to itself and the field would stay empty",
      "`this.` makes the assignment run faster",
      "It's purely stylistic; both lines do exactly the same thing",
      "`this.` is required in every assignment inside any method"
    ],
    "answer": 0,
    "explain": "When a parameter shares a name with a field, the unqualified name refers to the parameter. `this.owner` explicitly means the field. Writing `owner = owner` assigns the parameter to itself, leaving the field at its default - a silent bug with no error."
  },
  {
    "q": "You create two `Account` objects with identical owner and balance. What do `a == b` and `a.equals(b)` return, assuming `equals` is properly overridden?",
    "choices": [
      "`a == b` is false (different objects in memory); `a.equals(b)` is true (same values)",
      "Both are true - identical data means identical objects",
      "Both are false - Java never considers separate objects equal",
      "`a == b` is true and `a.equals(b)` is false"
    ],
    "answer": 0,
    "explain": "`==` compares object identity: two separate objects are never `==` even with identical data. A properly overridden `.equals()` compares the actual field values, so it returns true. This `==` vs `.equals()` split is the #1 Java beginner trap."
  },
  {
    "q": "Why must you override `hashCode` whenever you override `equals`?",
    "choices": [
      "Hash-based collections (HashMap, HashSet) require equal objects to have equal hash codes - override only `equals` and lookups silently break",
      "`hashCode` is what makes objects print readably",
      "The compiler refuses to compile a class that overrides only one of them",
      "`hashCode` controls how the constructor initializes fields"
    ],
    "answer": 0,
    "explain": "It's a contract: equal objects must return equal hash codes. HashMap/HashSet use hashCode to place objects in buckets, so if two 'equal' objects hash differently, a set stores both as distinct and a map lookup fails to find a key that's really there. Always change the pair together."
  }
]
```


---

# Inheritance & Interfaces - Sharing Behavior

Phase 5 built one self-contained object: bundle the data, guard it, give it behavior. This phase is about
relationships *between* objects - how one class builds on another, and how unrelated classes can promise
the same capability. These are Java's two mechanisms for sharing behavior, and the valuable thing to take
away isn't the syntax - it's knowing *which one to reach for*, since that judgment call separates clean
Java from the tangled inheritance towers that give OOP a bad name.

The mental model: two ways to say "this thing is related to that thing." One is **"is a kind of"** - a
`Dog` *is a kind of* `Animal`, so it inherits what an animal does. The other is **"is capable of"** - a
`Dog` *is capable of* making a sound, and so is a car horn, sharing nothing else. Inheritance handles the
first; interfaces the second. Most beginners over-use the first and under-use the second.

## Inheritance: building on a class with `extends`

**What it actually is.** **Inheritance** lets one class - the **subclass** - take everything a
**superclass** has and add to or change it. Write `class Dog extends Animal`, and `Dog` automatically gets
`Animal`'s behavior without copying a line. `super` lets the subclass reach back to the superclass, most
often to call its constructor.

**When it's the right tool.** Inheritance models a genuine **"is-a"** relationship: a `Dog` *is an*
`Animal`, a `SavingsAccount` *is an* `Account`. Can't say "X is a kind of Y" with a straight face?
Inheritance is the wrong tool - a warning we'll return to, since it's the one that matters most.

```java
public class Animal {
    protected String name;            // protected = visible to subclasses

    public Animal(String name) {
        this.name = name;
    }

    public void describe() {
        System.out.println(name + " is an animal.");
    }
}
```
```java
public class Dog extends Animal {     // Dog IS A kind of Animal
    public Dog(String name) {
        super(name);                  // call Animal's constructor to set up the inherited part
    }

    public void fetch() {             // brand-new behavior, only Dogs have it
        System.out.println(name + " fetches the ball.");
    }
}
```
```java
public class Main {
    public static void main(String[] args) {
        Dog rex = new Dog("Rex");
        rex.describe();   // inherited from Animal - never written in Dog
        rex.fetch();      // added by Dog
    }
}
```
```console
$ java Main.java
Rex is an animal.
Rex fetches the ball.
```
*What just happened:* `Dog extends Animal`, so `rex` got `describe()` for free - it lives in `Animal`, but
`rex` calls it as its own. `super(name)` handed the name up to `Animal`'s constructor. Then `Dog` added
`fetch()`, behavior no plain `Animal` has. That's inheritance: reuse what the superclass does, then extend
it.

📝 **`protected`** - a third visibility level alongside `public` and `private`, hidden from the outside
world but *visible to subclasses* - why `Dog`'s `fetch()` could read `name` directly. Use it sparingly;
`private` plus a getter is often still cleaner.

## Overriding: replacing an inherited method

Inheriting a method as-is is useful, but the real power is *changing* it: a subclass can **override** a
superclass method, providing its own version that runs instead.

**What it actually is.** **Overriding** means a subclass redefines an inherited method with the exact same
signature (name and parameters), giving it different behavior. Mark it with `@Override` so the compiler
verifies you matched an inherited method.

⚠️ **Don't confuse overriding with overloading (Phase 4).** They sound alike and mean opposite things.
**Overloading** is *several methods with the same name but different parameters* in one class
(`print(int)`, `print(String)`) - the compiler picks by the arguments. **Overriding** is *one signature,
redefined in a subclass* - Java picks at runtime by the object's actual type. Overloading is compile-time
convenience; overriding is the engine behind polymorphism.

```java
public class Animal {
    protected String name;

    public Animal(String name) { this.name = name; }

    public String speak() {           // the default sound
        return name + " makes a sound.";
    }
}
```
```java
public class Dog extends Animal {
    public Dog(String name) { super(name); }

    @Override
    public String speak() {           // replace Animal's version for Dogs
        return name + " says: Woof!";
    }
}

public class Cat extends Animal {
    public Cat(String name) { super(name); }

    @Override
    public String speak() {
        return name + " says: Meow!";
    }
}
```
```java
public class Main {
    public static void main(String[] args) {
        Animal a = new Dog("Rex");    // declared Animal, but it's really a Dog
        Animal b = new Cat("Mia");

        System.out.println(a.speak());   // which speak() runs?
        System.out.println(b.speak());
    }
}
```
```console
$ java Main.java
Rex says: Woof!
Mia says: Meow!
```
*What just happened:* Both `a` and `b` are *declared* as `Animal`, yet `a.speak()` ran `Dog`'s version and
`b.speak()` ran `Cat`'s. Java looked at the object's **actual runtime type**, not the declared type. This is
**dynamic dispatch**: which method body runs is decided at runtime, based on what the object truly is -
the whole point of the next section.

💡 **Why `@Override` earns its keep.** Optional, but always write it. Misspell the method name or get a
parameter wrong, and you've quietly created a *new* method while the inherited one still runs. `@Override`
makes the compiler catch that, turning a silent runtime bug into a compile error.

## Polymorphism: one type, many behaviors

This is the payoff - everything above was setup for this, the reason inheritance exists.

📝 **Polymorphism** - a variable of a *supertype* can hold an object of *any subtype*, and calling an
overridden method on it runs the version matching the object's **real** type. One line, `thing.speak()`,
does the right thing for a `Dog`, `Cat`, or any future animal, without ever knowing which it's dealing with.
("Polymorphism" is Greek for "many shapes": one variable, many concrete shapes underneath.)

**Why this is the whole point.** Without polymorphism, handling three animal types means three branches of
`if/else`. With it, you write the loop *once* against the supertype, and each object brings its own
behavior along. Add a `Cow` class next year and the loop doesn't change. You program against the general
idea; the specifics take care of themselves.

```java
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // A list of Animal - but each element is really a different subtype
        List<Animal> zoo = List.of(
            new Dog("Rex"),
            new Cat("Mia"),
            new Dog("Buddy")
        );

        for (Animal a : zoo) {        // we only know they're Animals
            System.out.println(a.speak());   // each runs ITS OWN speak()
        }
    }
}
```
```console
$ java Main.java
Rex says: Woof!
Mia says: Meow!
Buddy says: Woof!
```
*What just happened:* The loop variable `a` is typed `Animal`, so the loop has no idea whether it holds a
dog or a cat. Yet each `a.speak()` produced the correct sound, because dynamic dispatch resolved the call
against each object's real type at runtime. *This* is why we bothered with inheritance and overriding: one
loop against the supertype handles every subtype - including ones that don't exist yet.

## Interfaces: a contract any class can sign

Inheritance has a hard limit: a class can `extend` **exactly one** superclass. Java has no multiple
inheritance of classes - a deliberate choice avoiding a famous category of ambiguity bugs. But a class
often needs several roles. That's what interfaces are for.

📝 **Interface** - a *contract*: a named list of method signatures a class promises to provide. A class that
`implements` an interface must supply a body for each method, or it won't compile. Unlike `extends`, a
class can `implements` **many** interfaces at once. The interface says *what* must be possible; each class
decides *how*.

```java
public interface Drawable {       // the contract: "anything drawable can draw itself"
    void draw();                  // no body - just the promise
}
```
```java
public class Circle implements Drawable {
    @Override
    public void draw() {
        System.out.println("Drawing a circle ◯");
    }
}

public class Square implements Drawable {
    @Override
    public void draw() {
        System.out.println("Drawing a square ▢");
    }
}
```
```java
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Drawable> shapes = List.of(new Circle(), new Square());
        for (Drawable d : shapes) {
            d.draw();             // polymorphism again - via interface this time
        }
    }
}
```
```console
$ java Main.java
Drawing a circle ◯
Drawing a square ▢
```
*What just happened:* `Circle` and `Square` share no common parent - they're unrelated. But both *signed
the same contract* by implementing `Drawable`, so we treat them uniformly and loop over them with the same
polymorphism you just saw. The interface gave shared behavior *without* forcing an "is-a" family tree - its
superpower: grouping by *capability*, not ancestry.

💡 **Default methods.** Since Java 8, an interface method can ship with a body using `default` - a fallback
implementation classes inherit unless they override it, mainly so library authors can add a method to an
existing interface without breaking every implementing class. You'll see it in the standard library
(`List.sort`); reach for it rarely yourself.

## Abstract classes - and the call between the two

There's a middle ground between a fully-built class and a pure interface: the **abstract class**.

**What it actually is.** An `abstract` class is one you *can't instantiate directly* - `new Animal(...)` is
a compile error if `Animal` is abstract. It exists only to be extended, mixing **shared state** (fields,
real constructors) and **shared code** (fully-written methods) with `abstract` methods that have no body
and *force* every subclass to supply one.

```java
public abstract class Shape {
    private final String name;        // shared STATE - interfaces can't hold this

    public Shape(String name) {       // shared constructor
        this.name = name;
    }

    public abstract double area();    // no body - every subclass MUST implement this

    public void describe() {          // shared CODE, reused by all subclasses
        System.out.println(name + " has area " + area());
    }
}
```
```java
public class Rectangle extends Shape {
    private final double w, h;

    public Rectangle(double w, double h) {
        super("Rectangle");
        this.w = w;
        this.h = h;
    }

    @Override
    public double area() {            // satisfy the abstract method
        return w * h;
    }
}
```
```java
public class Main {
    public static void main(String[] args) {
        // Shape s = new Shape("x");  // ERROR: can't instantiate an abstract class
        Shape r = new Rectangle(3, 4);
        r.describe();                 // uses shared describe(), which calls our area()
    }
}
```
```console
$ java Main.java
Rectangle has area 12.0
```
*What just happened:* `Shape` can't be built on its own - a half-finished blueprint. `Rectangle` finished
it by implementing `area()`, and inherited the ready-made `describe()` and `name` field in return.
`describe()` calls `area()` and gets `Rectangle`'s version via dynamic dispatch: the abstract class wrote
shared logic once, letting each subclass fill in the one differing piece.

💡 **The plain guidance: interface or abstract class?**
- **Interface** for a *capability or contract* - "can be drawn," "can be compared," "can be saved." Lighter
  and more flexible; a class can implement many, and modern default methods cover most cases interfaces
  once couldn't. **When unsure, prefer an interface.**
- **Abstract class** only when subclasses genuinely need to *share state or substantial code* in a common
  base - like `Shape`'s `name` field and `describe()` method. The one-superclass limit is the price you pay.

⚠️ **Favor composition and interfaces over deep inheritance.** The single most common OOP mistake is
building tall hierarchies - `A extends B extends C extends D` - to share code. They're rigid (one
superclass, forever), brittle (a tweak in `B` ripples everywhere), and force "is-a" relationships that
often aren't true. The modern habit: model *capabilities* with interfaces, and when one object needs
another's behavior, **hold it as a field** (composition) instead of inheriting from it.

## Recap

1. **Inheritance** (`class Dog extends Animal`) lets a subclass reuse and extend a superclass; `super(...)`
   calls the superclass constructor. Use it only for a true **"is-a"** relationship.
2. **Overriding** redefines an inherited method (mark it `@Override`); it's resolved at runtime by the
   object's real type - distinct from **overloading**, which is same-name methods chosen at compile time.
3. **Polymorphism** is the payoff: a supertype variable holds any subtype, and the right overridden method
   runs automatically - so you write one loop that handles every subtype, present and future.
4. An **interface** is a contract of methods a class promises (`implements`); a class can implement **many**
   interfaces, grouping unrelated classes by *capability* rather than ancestry. `default` methods add an
   optional body.
5. An **abstract class** can't be instantiated and forces subclasses to implement its `abstract` methods,
   but can also share state and code in a base.
6. 💡 Prefer an **interface** for a capability (and when in doubt); use an **abstract class** for shared
   state/code. ⚠️ Favor **composition + interfaces** over deep inheritance towers.

You can now make classes share behavior two ways - and, more importantly, choose between them with
judgment instead of habit. Next: what happens when things go wrong - errors, exceptions, reading and
writing data.

## Quick check

Test yourself on the distinctions most likely to trip you in real code:

```quiz
[
  {
    "q": "A variable is declared `Animal a` but actually holds a `Dog` object, and `Dog` overrides `speak()`. When you call `a.speak()`, which version runs?",
    "choices": [
      "Dog's version - Java dispatches on the object's real runtime type, not the declared type",
      "Animal's version - the declared type decides which method runs",
      "Neither; it's a compile error because the types don't match",
      "Both, one after the other, starting with Animal's"
    ],
    "answer": 0,
    "explain": "This is dynamic dispatch, the engine behind polymorphism. Java looks at what the object actually is at runtime (a Dog), not how the variable is declared (Animal), so Dog's overridden speak() runs. That's exactly why one loop over a List<Animal> can handle every subtype correctly."
  },
  {
    "q": "What's the difference between overriding and overloading?",
    "choices": [
      "Overriding redefines an inherited method (same signature) in a subclass, resolved at runtime; overloading is several same-name methods with different parameters in one class, resolved at compile time",
      "They're two words for the same thing",
      "Overloading replaces a superclass method; overriding adds a new parameter list",
      "Overriding works only on static methods; overloading only on instance methods"
    ],
    "answer": 0,
    "explain": "Overriding = one signature redefined in a subclass, picked at runtime by the object's real type (the basis of polymorphism). Overloading = same name, different parameter lists in one class, picked at compile time by the arguments. They sound alike but do opposite things."
  },
  {
    "q": "You need several unrelated classes to share a capability, and a class already extends something else. Interface or abstract class?",
    "choices": [
      "Interface - a class can implement many interfaces, and they group classes by capability rather than ancestry",
      "Abstract class - it's always the better choice for shared behavior",
      "Neither works; you must copy the methods into each class",
      "Abstract class, because a class can extend several of them at once"
    ],
    "answer": 0,
    "explain": "A class can extend only one class but implement many interfaces, so when classes are unrelated (or already extend something), an interface is the fit - it describes a capability without forcing an is-a family tree. Reach for an abstract class only when subclasses need to share actual state or code in a common base."
  }
]
```


---

# Errors & I/O - Exceptions, Resources & Files

Every program eventually meets the moment something goes wrong: a file isn't there, a number won't parse, a network call times out. Languages disagree, sometimes fiercely, on what to *do* then. If you've seen the Go guide, you know one answer - [errors are values](/guides/go-from-zero) you return and check by hand. Java takes the other big path, worth stating up front since it shapes everything here:

> **In Java, an error is a thrown object that unwinds the stack until something catches it.**

When a method hits trouble, it doesn't return a special value - it *throws*. Normal execution stops dead, and the runtime walks back up the call chain, abandoning each method, looking for code that said "I'll handle this." Find a handler, control jumps there; find nobody, the program crashes and prints a stack trace. Once that picture is in your head, `try`/`catch`/`finally`, the checked/unchecked split, and `try`-with-resources stop being syntax to memorize and become obvious consequences of one idea.

## Exceptions - Java's error model

📝 **An exception** is an object (a subclass of `Throwable`) representing something going wrong. *Throwing* it stops normal flow and searches upward through the call stack for a *handler*. *Catching* it says "stop unwinding here, I've got this."

Contrast this with Go: a function that can fail hands back `(result, err)` and you check `if err != nil` right there - the error travels *with* the return value. In Java the failure travels *instead of* it: the method never returns normally, ripping through every intermediate method until it reaches a `catch`. The upside: let an error blow past five layers with nothing useful to say, and handle it once, where it matters. The cost: control flow becomes invisible - an innocent line might launch an exception three calls deep.

You contain that with three keywords:

- `try` - wrap the code that might throw.
- `catch` - handle a specific exception type if it's thrown.
- `finally` - run cleanup *no matter what* (threw or not, caught or not).

**A real example.**

```java
public class Divide {
    public static void main(String[] args) {
        try {
            int[] nums = {10, 0};
            System.out.println("result: " + (nums[0] / nums[1]));
        } catch (ArithmeticException e) {
            System.out.println("caught: " + e.getMessage());
        } finally {
            System.out.println("finally always runs");
        }
        System.out.println("program continues");
    }
}
```
```console
$ java Divide.java
caught: / by zero
finally always runs
program continues
```
*What just happened:* `nums[0] / nums[1]` is `10 / 0`, throwing an `ArithmeticException`. The division never completed - control jumped straight to the matching `catch`, which printed `e.getMessage()`. Then `finally` ran (it always does), and since we *caught* the exception, the program continued. Remove the `try`/`catch` and it unwinds all the way out of `main`, crashing with a stack trace like this:

```console
$ java Divide.java
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Divide.main(Divide.java:5)
```
*What just happened:* With no handler, the exception walked up past `main`, hit the top of the stack, and the JVM printed the exception type, message, and the exact line each frame was on. Your most useful debugging tool - read it top-down: the first line is *what* went wrong, the `at ...` lines trace *where*, most recent first.

## Checked vs unchecked - the split the compiler enforces

Here's the part that's genuinely Java's own, and the first thing that surprises newcomers. Java sorts exceptions into two camps, treated completely differently *at compile time*.

📝 **Checked exceptions** (subclasses of `Exception` but not `RuntimeException`, e.g. `IOException`) are ones the compiler forces you to deal with: any method that might throw one must either `catch` it or *declare* it with `throws` in its signature. Forget to, and your code won't compile. **Unchecked exceptions** (subclasses of `RuntimeException`, e.g. `NullPointerException`, `ArithmeticException`, `IllegalArgumentException`) carry no such obligation - you *may* catch them, but the compiler won't make you.

The intended dividing line: checked exceptions are for *recoverable, expected* conditions outside your control - a file might not exist - and a caller ought to have a plan. Unchecked exceptions are for *programming bugs* - a null you should have checked, an index past an array's end, an invalid argument. You can't "recover" from a bug; you fix it.

**A real example.** Watch the compiler refuse the checked one:

```java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class Checked {
    // This method does NOT compile - Files.readString can throw IOException,
    // and we neither catch it nor declare it.
    static String load() {
        return Files.readString(Path.of("notes.txt"));
    }
}
```
```console
$ java Checked.java
Checked.java:9: error: unreported exception IOException; must be caught or declared to be thrown
        return Files.readString(Path.of("notes.txt"));
                               ^
1 error
```
*What just happened:* `Files.readString` declares `throws IOException` - checked - so the compiler demanded we acknowledge it. We did neither, so compilation failed *before the program ran*. Fix it by adding `throws IOException` to our method or wrapping the call in `try`/`catch`. Compare to `10 / 0`: `ArithmeticException` is unchecked, so the compiler said nothing - it only blew up at runtime.

💡 **Why checked exceptions are controversial.** The idea is sound: the compiler guarantees you can't *accidentally* ignore a failure mode the API author thought important. In practice, many find them noisy - they push `throws` up through every layer, and tired programmers "shut the compiler up" with an empty `catch {}` that swallows the very error checked exceptions existed to surface. That's why Kotlin and Scala dropped them entirely, and why much Java code wraps checked exceptions in unchecked ones. Know which camp an exception is in - the compiler will tell you.

## Throwing - and writing your own exceptions

You're not limited to catching exceptions the library throws; you throw your own with `throw`. The most common case is rejecting bad input *the moment you detect it*, rather than letting a garbage value slither deeper into your program where it causes a confusing failure far from the cause.

For built-in cases, reach for the standard unchecked types - `IllegalArgumentException` (a caller passed something invalid) and `IllegalStateException` (the object isn't in a state where this call makes sense) - covering a huge fraction of real code.

```java
public class Account {
    static int withdraw(int balance, int amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("amount must be positive, got " + amount);
        }
        if (amount > balance) {
            throw new IllegalArgumentException("insufficient funds");
        }
        return balance - amount;
    }

    public static void main(String[] args) {
        System.out.println(withdraw(100, 30));   // fine
        System.out.println(withdraw(100, -5));   // throws
    }
}
```
```console
$ java Account.java
70
Exception in thread "main" java.lang.IllegalArgumentException: amount must be positive, got -5
	at Account.withdraw(Account.java:4)
	at Account.main(Account.java:14)
```
*What just happened:* The first `withdraw` returned `70` normally. The second hit `amount <= 0`, so `throw new IllegalArgumentException(...)` fired, constructing and launching an exception with our message. `withdraw` never returned a value - the throw replaced it - and since `main` didn't catch it, the program crashed with our message attached. The key habit: *fail fast and loud* - validate at the boundary so the stack trace points at the real culprit.

**Writing a custom exception** is just subclassing. Do it when no built-in type names your error well and callers might want to catch *this specific thing*:

```java
class InsufficientFundsException extends RuntimeException {
    InsufficientFundsException(String message) {
        super(message);   // hand the message up to the base class
    }
}
```
*What just happened:* We extended `RuntimeException`, making our exception *unchecked*. The one-line constructor passes a message up to the parent so `getMessage()` works. Now `throw new InsufficientFundsException("balance too low")` reads like a sentence, and a caller can write `catch (InsufficientFundsException e)` to handle exactly that case. (Extend `Exception` instead for *checked*.) ⚠️ Don't manufacture custom exceptions for every error - a built-in type with a good message is usually clearer and less code.

## try-with-resources - cleanup that can't leak

Files, database connections, network sockets - anything you *open*, you must *close*, or you leak OS handles until your program (or the machine) runs out. The naive approach is a `finally` block calling `close()`, but that's verbose and easy to get subtly wrong (what if `close()` itself throws?). Java's purpose-built answer is **try-with-resources**.

📝 **try-with-resources** is a `try` with a parenthesized declaration: `try (var thing = open()) { ... }`. Any resource declared there is *automatically closed* when the block exits - normally or via exception - as long as it implements `AutoCloseable` (every file, stream, and connection in the standard library does). A structural guarantee you can't forget the close.

💡 This is the Java answer to "always close what you open." Declare the resource in the `try` header and the compiler wires up cleanup, even on the exception path. If you remember one resource-handling pattern, make it this one.

```java
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class ReadLines {
    public static void main(String[] args) throws IOException {
        try (BufferedReader reader = Files.newBufferedReader(Path.of("notes.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println("line: " + line);
            }
        }   // reader.close() happens here, automatically - even if readLine threw
    }
}
```
```console
$ cat notes.txt
buy milk
call dentist
$ java ReadLines.java
line: buy milk
line: call dentist
```
*What just happened:* `Files.newBufferedReader` opened the file (a resource), and since we declared it inside `try (...)`, Java guaranteed `reader.close()` ran the instant the block ended - loop finished cleanly or `readLine` threw partway through. We never typed `close()` or wrote a `finally`. `throws IOException` on `main`: reading can fail with a checked exception, and here we chose to declare rather than catch it - fine for a small program. The leak-proof part is the parenthesized declaration; the rest is ordinary loop code.

## File I/O - the modern way with `java.nio.file.Files`

Older Java tutorials drown you in `FileReader`, `FileWriter`, `BufferedReader`, and streams wrapped in streams. For common cases, ignore all that: `java.nio.file.Files` gives clean, one-call methods built around `Path` objects. Reach for these first.

The three you'll use constantly:

- `Files.readString(path)` - read an entire (small) text file into one `String`.
- `Files.readAllLines(path)` - read a file into a `List<String>`, one entry per line.
- `Files.writeString(path, text)` - write a `String` to a file, creating or overwriting it.

```java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public class FileDemo {
    public static void main(String[] args) throws IOException {
        Path path = Path.of("greeting.txt");

        Files.writeString(path, "hello\nworld\n");   // create + write in one call

        String whole = Files.readString(path);       // entire file as one String
        System.out.print("readString gives:\n" + whole);

        List<String> lines = Files.readAllLines(path);  // one String per line
        System.out.println("line count: " + lines.size());
        System.out.println("first line: " + lines.get(0));
    }
}
```
```console
$ java FileDemo.java
readString gives:
hello
world
line count: 2
first line: hello
```
*What just happened:* `Files.writeString` created `greeting.txt` and wrote both lines in a single call - no stream to open or close, since these methods manage the resource internally. `Files.readString` handed the whole file back as one `String` (newlines included); `Files.readAllLines` split it into a `List<String>` so we could count lines and index into them. Each declares `throws IOException` - the file might not exist, the disk might be full - exactly why `main` declares it too. For genuinely large files you'd stream (`Files.lines(path)` returns a lazy `Stream<String>`), but for config files and most everyday work, these three methods are all you need.

⚠️ **The one runtime exception you'll meet most.** `NullPointerException` - thrown the instant you call a method or read a field on a `null` reference. A `null` path, a map lookup returning nothing, a method returning `null` you forgot to check - all roads lead to the dreaded NPE, the most common exception in production Java. It's *unchecked*, so the compiler gives no warning; it just detonates at runtime. Taming `null` gets its own treatment in [Phase 9](09-idioms-and-gotchas.md).

## Recap

1. **Exceptions are Java's error model** - a thrown object unwinds the stack until a `catch` handles it, or the program crashes with a stack trace. Opposite of Go's "errors are values you check inline."
2. **`try` / `catch` / `finally`** - wrap risky code, handle specific types, and `finally` runs cleanup on every path. Read stack traces top-down: *what* first, then the *where* trail.
3. **Checked vs unchecked is the Java-specific split** - checked exceptions (`IOException`) *must* be caught or declared with `throws`, compiler-enforced; unchecked (`RuntimeException`, `NullPointerException`) needn't be. Checked exceptions are controversial since that obligation can become noise.
4. **Throw your own** with `throw new IllegalArgumentException(...)` to fail fast at the boundary; write a custom exception (subclass `RuntimeException` or `Exception`) only when no built-in type fits.
5. **try-with-resources** - `try (var r = open()) { ... }` auto-closes anything `AutoCloseable`, on every exit path - the leak-proof way to handle files and connections.
6. **`java.nio.file.Files`** - `readString`, `readAllLines`, `writeString` cover everyday file I/O in one call each. ⚠️ Watch for `NullPointerException`, the most common runtime exception - more in Phase 9.

You now write code that fails clearly and cleans up after itself. Next: the *toolbox* around the language - how Java projects organize into packages and build with the tools the ecosystem actually uses.

## Quick check

Test yourself on the one idea that defines this phase - how Java handles failure:

```quiz
[
  {
    "q": "In Java, what happens when a method throws an exception that nothing catches?",
    "choices": [
      "The exception unwinds the entire call stack and the program crashes with a stack trace",
      "The method returns the special value null instead",
      "The exception is silently ignored and execution continues on the next line",
      "The compiler refuses to build the program until you add a return value"
    ],
    "answer": 0,
    "explain": "An uncaught exception keeps unwinding upward through each calling method until it leaves main, at which point the JVM crashes the program and prints a stack trace (type, message, and the line of each frame). Catching it anywhere along the way stops the unwind."
  },
  {
    "q": "What's the practical difference between a checked exception (like IOException) and an unchecked one (like NullPointerException)?",
    "choices": [
      "A checked exception must be caught or declared with throws, or the code won't compile; an unchecked one carries no such requirement",
      "A checked exception is faster because the compiler optimizes it",
      "An unchecked exception always crashes the program, while a checked one never does",
      "There's no real difference - the terms are interchangeable"
    ],
    "answer": 0,
    "explain": "The compiler enforces checked exceptions: any method that might throw one must catch it or declare throws. Unchecked exceptions (subclasses of RuntimeException) carry no compile-time obligation - they only surface at runtime, which is exactly why a forgotten null check (NullPointerException) compiles fine but blows up later."
  },
  {
    "q": "Why prefer try-with-resources - `try (var r = Files.newBufferedReader(path)) { ... }` - over opening a file and closing it yourself?",
    "choices": [
      "It automatically closes the resource on every exit path, including when an exception is thrown, so the file can't leak",
      "It makes file reading run significantly faster",
      "It converts checked exceptions into unchecked ones automatically",
      "It lets you skip importing the java.nio.file package"
    ],
    "answer": 0,
    "explain": "Any resource declared in the try header (that implements AutoCloseable) is closed automatically when the block exits - normally or via exception - so you can never forget the close or leak a handle. You don't write close() at all; the structure guarantees it."
  }
]
```


---

# Packages, Build & Tooling - From Files to a Real Project

Up to now your Java has lived in loose `.java` files, compiled with `javac` and run with `java`. That works for learning, but falls apart once a project grows past a dozen classes or needs a library someone else wrote - and *every* real project needs both.

This phase is about the gap between "I can write a class" and "I can work on an actual Java project." The mental model: a real Java project isn't a pile of files you compile by hand. It's a *named, organized structure* (packages) that a *build tool* (Maven or Gradle) compiles, tests, and bundles for you - pulling in other people's code (dependencies) automatically. Once you see how those pieces fit, an unfamiliar Java repo stops being intimidating.

## Packages - namespaces that keep classes from colliding

**What it actually is.** A **package** is a named group of related classes, declared as the very first line of a file: `package com.example.app;`. That name does two jobs at once - groups your classes logically, and maps directly to a folder on disk. A class in package `com.example.app` lives in `com/example/app/`.

📝 **Package** - a namespace for your classes, written as a dotted name like `com.example.billing`. It maps one-to-one to a directory path (`com/example/billing/`), groups related code, and prevents name clashes: your `User` and a library's `User` coexist because their *full* names differ (`com.example.User` vs `org.lib.User`).

**Why this exists.** Without packages, every class name would need to be globally unique - impossible once you pull in libraries, since two might both define a `Logger`. Packages also control visibility: a class or member with no access modifier (met in [Phase 5](05-classes-and-objects.md)) is *package-private* - visible only within the same package.

To use a class from another package, you `import` it:

```java
package com.example.app;

import java.util.ArrayList;       // pull in one class from java.util
import com.example.model.User;    // pull in your own class from another package

public class Main {
    public static void main(String[] args) {
        ArrayList<User> users = new ArrayList<>();
        users.add(new User("Ada"));
        System.out.println("users: " + users.size());
    }
}
```

*What just happened:* The `package` line says "this file belongs to `com.example.app`," so on disk it sits at `com/example/app/Main.java`. The two `import` lines bring `ArrayList` and `User` into scope by short name, so we write `ArrayList` instead of `java.util.ArrayList`. Imports don't *do* anything at runtime - a compiler convenience, telling it which full name a short name refers to. (One package is special: everything in `java.lang` - `String`, `System`, `Integer` - is imported automatically.)

💡 **Key point.** The package name *is* the folder structure. A file saying `package com.example.app;` but sitting in the wrong directory fails to compile.

## The classpath - how the JVM finds your classes

You've written packages and imports. But when the program runs, how does the JVM locate `com.example.model.User` on disk, or a class buried inside some downloaded library? The answer: the **classpath**, explaining one of the most common beginner errors.

**What it actually is.** The **classpath** is the list of locations - folders and JAR files - where the JVM looks for compiled `.class` files, at compile time and run time. Referencing `com.example.model.User`, the JVM walks each classpath entry looking for `com/example/model/User.class`. Find it, great. Miss it, and you get the infamous `ClassNotFoundException` or `NoClassDefFoundError`.

📝 **Classpath** - the search path the JVM uses to find classes: a list of roots (directories and `.jar` files). The JVM resolves a class's full name into a relative path under each root until it finds the matching `.class`. Set it with `-cp` (or `-classpath`), or the `CLASSPATH` environment variable.

**A real example.** Say you compiled into a folder `out/` and depend on a library `gson.jar`. Running by hand looks like this:

```bash
java -cp "out:libs/gson.jar" com.example.app.Main
```

```console
users: 1
```

*What just happened:* The `-cp` flag listed two places to find classes: your compiled output (`out/`) and the Gson JAR. The JVM searched both roots to resolve every class the program touched. The final argument is the *full name* of the class with `main()`, not a file path. Miss one classpath entry and the program dies with `ClassNotFoundException`. (On Windows the separator is `;` instead of `:`.)

⚠️ **This is exactly why managing the classpath by hand doesn't scale.** A toy program has one or two entries; a real one has *dozens* of library JARs, each possibly needing *other* JARs, all at specific versions. Assembling and maintaining that string is miserable and error-prone - the entire reason build tools exist.

## Build tools: Maven & Gradle

A **build tool** does everything tedious about turning source into a runnable, shippable artifact - so you never touch `javac` or a raw classpath again.

**What it actually does.** Given a single config file, a build tool will: compile your code, **download and manage dependencies** (resolving the whole classpath, versions and all), run tests, and package the result into a JAR. One command does the lot. **Maven** and **Gradle** dominate Java.

📝 **Build tool** - automation that compiles, tests, packages, and (crucially) manages dependencies from a declarative config file. You describe *what* your project needs; the tool figures out *how* to assemble it, including the classpath.

**Maven** is the older, convention-heavy one. You describe your project in a `pom.xml` file (XML). Maven prizes "convention over configuration": follow its standard folder layout (`src/main/java`, `src/test/java`) and you barely configure anything. Here's the part of a `pom.xml` declaring a dependency:

```xml
<dependencies>
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
        <version>2.13.1</version>
    </dependency>
</dependencies>
```

*What just happened:* Those three coordinates - `groupId`, `artifactId`, `version` - uniquely identify Gson. Maven reads them, fetches from **Maven Central** (a huge public repository of open-source Java libraries), downloads that exact version, and adds it to your classpath automatically. No JAR file, no `-cp` flag - add five lines of XML and let Maven fetch. That single idea - "name a library by coordinates, the tool wires it up" - makes Java's ecosystem usable.

**Gradle** is the newer alternative. Instead of XML, you write the build in a DSL - Groovy or Kotlin - more programmable and flexible. The same dependency in Gradle's Kotlin DSL is one line:

```text
dependencies {
    implementation("com.google.code.gson:gson:2.13.1")
}
```

*What just happened:* Same three coordinates (`group:artifact:version`), same result - Gradle resolves from Maven Central and builds the classpath. The difference is style: Gradle's build files are real code, expressing logic XML can't, at the cost of being less predictable than Maven's rigid conventions. Android standardized on Gradle; much server-side Java still runs on Maven. Both solve the same problem; the choice is mostly team preference.

💡 **Key point.** In real projects you almost never call `javac` directly. Run `mvn package` or `./gradlew build`, and the tool compiles, resolves every dependency, runs your tests, and produces a JAR - all from one config file.

## JARs & dependencies - bundling code to share and ship

The build tool keeps producing something called a JAR - worth knowing exactly what that is.

**What it actually is.** A **JAR** (Java ARchive) is just a ZIP file with a `.jar` extension, holding compiled `.class` files plus a little metadata. It's how Java code is packaged for sharing - every library on Maven Central is a JAR, and your own built project becomes one too. Put a JAR on the classpath, and the JVM reads classes straight out of it.

📝 **JAR** - a zipped bundle of compiled `.class` files (and resources), the unit of distribution in Java: one file you can drop on a classpath, publish to a repository, or hand to someone else.

A normal library JAR contains only *that* library's classes. But to *run* an application you need it plus everything it depends on. That's where a **fat JAR** (*uber JAR*) comes in: a single JAR bundling your code *and* all its dependencies, running standalone with nothing else on the classpath:

```bash
java -jar myapp.jar
```

```console
Server started on port 8080
```

*What just happened:* `java -jar myapp.jar` ran the application from one self-contained file - no `-cp` listing a dozen library JARs, since they're all *inside* `myapp.jar`. Build tools produce these with a plugin (Maven's Shade, Gradle's Shadow). Simplest way to deploy a Java service: build one fat JAR, copy it to a server, run.

One more piece you get for free: **transitive dependencies**. Depend on library A, and if A depends on B and C, the build tool pulls those in automatically - you only declared A. The tool resolves the whole graph from Maven Central. (This has a dark side - version conflicts deep in the tree, "JAR hell" - but declaring one library quietly brings its friends along.)

## The wider toolchain

Beyond the language and the build tool, Java has a mature, batteries-included ecosystem. A quick orientation so the names aren't a mystery later:

- **IDEs.** Most Java is written in a full IDE rather than a plain editor, since the language's verbosity pays off when a tool understands it deeply. **IntelliJ IDEA** is the de facto standard (Eclipse and VS Code with Java extensions are common too). The IDE handles imports, refactoring, navigation, and runs your build tool for you.
- **Formatters & linters.** Tools like **Spotless** (formatting) and **Checkstyle** (style/lint rules) keep a codebase consistent and catch issues before review, wired into the build so rules are enforced, not argued about.
- **Testing & profiling.** The standard test framework is **JUnit**, and the JVM has excellent profiling tools for performance and memory problems - big enough topics for their own treatment in [Phase 16](16-testing-and-profiling.md).

💡 **Key point.** Java's ecosystem is old, deep, and well-supported: for almost any problem - JSON, HTTP, databases, logging, testing - there's a mature library a single dependency line away.

## Recap

1. A **package** (`package com.example.app;`) is a namespace that groups classes, maps to a folder on disk, controls visibility (package-private), and prevents name clashes. `import` brings other packages' classes into scope by short name.
2. The **classpath** is where the JVM searches for `.class` files and libraries; an entry it can't find is the cause of `ClassNotFoundException`. Managing it by hand doesn't scale - which is why build tools exist.
3. A **build tool** compiles, tests, packages, and - most importantly - resolves dependencies for you. **Maven** uses `pom.xml` (XML, convention-heavy); **Gradle** uses a Groovy/Kotlin DSL (flexible). Both pull libraries from **Maven Central** by `group:artifact:version` coordinates.
4. You rarely call `javac` directly in real projects; you run `mvn package` or `./gradlew build` instead.
5. A **JAR** is a zipped bundle of compiled classes - the unit of distribution. A **fat/uber JAR** bundles your dependencies inside so it runs standalone. **Transitive dependencies** are pulled in automatically.
6. The wider toolchain - **IntelliJ IDEA**, formatters/linters like **Spotless**/**Checkstyle**, and **JUnit** (Phase 16) - is mature and batteries-rich.

You can now read the shape of a real Java repository: the package folders, the `pom.xml` or `build.gradle`, the dependency list. Next: closing the guide with the idioms and gotchas that separate Java that *compiles* from Java that *looks like Java*.

## Quick check

Test yourself on the ideas that make a Java project a project, not just a folder of files:

```quiz
[
  {
    "q": "What does the line `package com.example.billing;` at the top of a file determine?",
    "choices": [
      "The class's namespace and the folder it must live in (com/example/billing/)",
      "Which build tool - Maven or Gradle - will compile the file",
      "The version of Java the file requires to run",
      "Which libraries the file is allowed to import"
    ],
    "answer": 0,
    "explain": "A package is a namespace that maps one-to-one to a directory path. The file must physically live at com/example/billing/, and the package name becomes part of every class's full name - which is how the JVM and tools locate it."
  },
  {
    "q": "Why do Maven and Gradle exist instead of just running `javac` and `java -cp` by hand?",
    "choices": [
      "They automatically resolve and download dependencies and build the whole classpath, which is unmanageable by hand once a project has many library versions",
      "They make Java code run faster than plain javac",
      "They are required by the JVM to load any class at all",
      "They replace the need to write packages and imports"
    ],
    "answer": 0,
    "explain": "A real project has dozens of library JARs at specific versions, plus their transitive dependencies. Assembling and maintaining that classpath manually is error-prone and miserable; the build tool resolves the whole dependency graph from Maven Central and wires it up for you."
  },
  {
    "q": "What is a 'fat JAR' (uber JAR), and why is it useful?",
    "choices": [
      "A single JAR bundling your code plus all its dependencies, so it runs standalone with `java -jar`",
      "A JAR that has been compressed twice to take less disk space",
      "A JAR containing only documentation and source, not compiled classes",
      "A JAR that automatically updates its dependencies at runtime"
    ],
    "answer": 0,
    "explain": "A normal library JAR holds only its own classes. A fat/uber JAR bundles your application together with every dependency inside one file, so you can deploy and run it with `java -jar myapp.jar` - no separate classpath of library JARs needed."
  }
]
```


---

# Idioms & Common Gotchas - Write It Like a Java Dev, Dodge the Traps

You can write Java that compiles and runs. This phase closes the gap between *that* and code that looks like a seasoned Java developer wrote it - plus the short list of traps that have caught every Java programmer alive. (Genuinely - the `==` versus `.equals()` one alone has cost the industry more debugging hours than anyone wants to count. You're about to skip past it.)

Two halves. First the **idioms** - conventions that make Java code feel coherent instead of arbitrary. The mental model: *Java rewards small contracts and unchanging data*. Depend on interfaces, not concrete classes; make things final and immutable so they can't change behind your back; say "no value" out loud with `Optional` instead of the silent landmine that is `null`.

Then a scannable **gotcha cheat-card** - surprises named *before* they bite, so you recognize one instead of staring at a stack trace. The mental model: *Java does exactly what the rules say, not what you assumed.*

## Idioms - the way it's written today

### Program to interfaces, not implementations

**What it actually is.** When declaring a variable, parameter, or return type, name the *interface* (`List`, `Map`, `Collection`) rather than the concrete class (`ArrayList`, `HashMap`). You still *create* the concrete thing with `new` - just don't let the rest of your code depend on which one.

```java
// Idiomatic: the type is the contract, not the implementation.
List<String> names = new ArrayList<>();

// Not idiomatic: now everything downstream is welded to ArrayList.
ArrayList<String> names2 = new ArrayList<>();
```
*What just happened:* both lines create an `ArrayList`, but the first *types* the variable as `List`. A method taking `List<String>` accepts an `ArrayList`, a `LinkedList`, or `List.of(...)` without changes, and you swap implementations later by editing one line. That's why you'll see `void process(List<Order> orders)` everywhere and almost never `void process(ArrayList<Order> orders)`.

💡 **Key point.** Declare with the most general interface that does the job (`List`, `Map`, `Set`, `Collection`), construct with the concrete class. Flexible outside, specific inside.

### Prefer immutability

**What it actually is.** An *immutable* object's state can't change after it's built. Mark fields `final`, don't expose setters, assign everything in the constructor - once it exists, it's frozen.

```java
public final class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int x() { return x; }
    public int y() { return y; }

    // "Change" returns a NEW Point instead of mutating this one.
    public Point withX(int newX) {
        return new Point(newX, y);
    }
}
```
*What just happened:* `Point` has no setters and every field is `final`, so once constructed it never changes. Need a different `x`? `withX` hands you a brand-new `Point`, leaving the original untouched. Sounds wasteful, but immutable objects are automatically thread-safe, safe to share and cache, and never in a half-updated state. When in doubt, make value objects immutable. (In [Phase 13](13-records-and-modern-java.md), `record` automates this pattern down to one line.)

⚠️ **Gotcha - `final` on a reference freezes the *reference*, not the object.** `final List<String> items = new ArrayList<>();` stops you reassigning `items`, but `items.add(...)` still works all day. `final` means "this variable always points at the same object," not "that object can't change." True immutability needs the object itself unchangeable (`List.copyOf(items)`).

### Use `Optional` instead of returning `null`

**What it actually is.** `Optional<T>` is a small box holding a value or explicitly empty. When a method might legitimately have "no answer" (no user found, no config set), returning `Optional<User>` instead of `User` (which might secretly be `null`) makes the absence *visible in the type*.

```java
import java.util.Optional;

Optional<String> findNickname(String user) {
    if (user.equals("ada")) return Optional.of("Countess");
    return Optional.empty();              // "no value" - said out loud
}

// Caller is forced to deal with the empty case:
String shown = findNickname("bob").orElse("(none)");
System.out.println(shown);
```
```console
(none)
```
*What just happened:* `findNickname` returns an `Optional<String>`, so its signature *announces* "there might be nothing here." The caller used `.orElse("(none)")` for a fallback. Compare that to returning `null`: nothing warns you, and the day you forget a null check, you get a `NullPointerException` in production instead of a compiler nudge.

💡 **Key point.** Use `Optional` for *return types* that may be absent. Skip it for fields or parameters (it adds ceremony without payoff there), and never call `.get()` without checking first - that just reinvents the NPE.

### Loop with the enhanced for and streams

**What it actually is.** The enhanced for-loop (`for (var x : items)`) iterates without a manual index. Streams go further: describe *what* to do to a collection - filter, map, collect - instead of spelling out *how* with a counter.

```java
import java.util.List;

List<String> names = List.of("ada", "bob", "cleo");

// Enhanced for - no index to fat-finger.
for (var name : names) {
    System.out.println(name.toUpperCase());
}

// Stream - describe the transformation as a pipeline.
List<String> longOnes = names.stream()
        .filter(n -> n.length() > 3)
        .toList();
System.out.println(longOnes);
```
```console
ADA
BOB
CLEO
[cleo]
```
*What just happened:* the enhanced for-loop walked the list with no `int i` to manage and no off-by-one risk. The stream expressed "keep names longer than three characters" as a readable pipeline - `filter` describes *intent*, not mechanics. Streams shine when chaining transformations; for a simple walk, the enhanced for is perfectly idiomatic. (Streams and lambdas get their own treatment in [Phase 12](12-streams-api.md).)

### Write meaningful `equals`, `hashCode`, and `toString`

**What it actually is.** By default, `equals` checks *identity*, and `toString` prints something useless like `Point@1b6d3586`. If two objects with the same field values should count as "equal" - especially bound for a `HashMap` or `HashSet` - override `equals` and `hashCode` together.

```java
import java.util.Objects;

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Point p)) return false;
    return x == p.x && y == p.y;
}

@Override
public int hashCode() {
    return Objects.hash(x, y);     // must agree with equals
}

@Override
public String toString() {
    return "Point(" + x + ", " + y + ")";
}
```
*What just happened:* `equals` now compares actual field values, so two `Point(1, 2)` objects are equal despite being separate objects in memory. `hashCode` uses the *same* fields - non-negotiable, since hash-based collections use `hashCode` to find the bucket and `equals` to confirm the match. Disagree, and your objects vanish into a `HashMap` unrecoverably. `toString` makes debugging humane. ([Records](13-records-and-modern-java.md) generate all three; until then, honor this contract.)

> 💡 The umbrella idiom: make intent explicit, illegal states impossible. Interfaces narrow the contract, immutability removes a whole class of "who changed this?" bugs, `Optional` makes absence visible, proper `equals`/`hashCode` makes equality mean what you mean. Favor composition over deep inheritance - clarity over cleverness.

## The gotcha cheat-card

> **Hit something baffling? Find the symptom here.** These trap *everyone* - recognizing them on sight is most of the battle.

| The trap | What bites you | The fix |
|---|---|---|
| `==` vs `.equals()` | `==` compares references, so equal-looking objects/strings can be "not equal" | Compare objects with `.equals()`; reserve `==` for primitives and identity |
| `null` / NPE | Calling a method on `null` throws `NullPointerException` at runtime | `Optional`, explicit null checks, `Objects.requireNonNull` on inputs |
| Autoboxing surprises | `Integer` caches −128..127, so `==` "works" then mysteriously breaks; unboxing a `null Integer` throws NPE | Use `.equals()` for `Integer`; keep arithmetic in primitives |
| Mutating a list while looping it | Removing during a for-each throws `ConcurrentModificationException` | Use `Iterator.remove()`, `removeIf(...)`, or collect-then-remove |
| Integer division | `5 / 2` is `2`, not `2.5` - the fraction is silently discarded | Cast one operand to `double`: `5 / 2.0` |
| Shared mutable state / arrays | Passing a list or array hands over a reference; the callee can mutate your data | Defensive copies (`List.copyOf`) or immutable objects |
| Catching `Exception` too broadly | A blanket `catch (Exception e) {}` swallows bugs and hides real failures | Catch specific types; never leave a catch block empty |

Now the *why* behind the sharpest three.

### `==` vs `.equals()`

The big one. `==` asks "are these the *same object* in memory?" `.equals()` asks "do these represent the *same value*?" For primitives `==` is correct and the only option. For *objects* - including `String` - `==` almost never means what you want.

```java
String a = new String("hello");
String b = new String("hello");

System.out.println(a == b);        // false - two different objects
System.out.println(a.equals(b));   // true  - same characters
```
```console
false
true
```
*What just happened:* `new String("hello")` built two separate `String` objects holding the same characters. `==` compared *references* - different objects, `false`. `.equals()` compared *contents* - same text, `true`. Dangerous because string *literals* (`"hello" == "hello"`) often return `true`, since Java pools identical literals, lulling beginners into thinking `==` works on strings. Then a string comes from user input, the pool doesn't apply, and `==` quietly returns `false`. **Always compare strings and objects with `.equals()`.**

### Autoboxing and the `Integer` cache

Java auto-converts between primitives (`int`) and object wrappers (`Integer`) - *autoboxing*. Convenient, and the source of two classic traps. First, `==` on two `Integer` objects compares references, not values - but the JVM *caches* small `Integer`s from −128 to 127, so `==` accidentally "works" in that range and breaks above it.

```java
Integer a = 100, b = 100;
System.out.println(a == b);    // true  - both from the cache (-128..127)

Integer c = 200, d = 200;
System.out.println(c == d);    // false - outside cache, two real objects
System.out.println(c.equals(d)); // true - value comparison, correct
```
```console
true
false
true
```
*What just happened:* `100` falls inside the cached range, so `a` and `b` point at the *same* cached `Integer` and `==` is `true`. `200` is outside the cache, so `c` and `d` are *separate* objects and `==` is `false` - even though both hold 200. Vicious, since it passes every test with small numbers and fails in production on large ones. Use `.equals()` for wrappers. Second trap: unboxing a `null` `Integer` (`int x = someInteger;` when null) throws a `NullPointerException` from a line that never mentions null. Keep arithmetic in primitives and sidestep both.

### Integer division

Dividing two `int`s gives an `int` - Java discards the remainder instead of producing a fraction. Bites every beginner computing an average or a percentage.

```java
int total = 5, count = 2;

System.out.println(total / count);          // 2   - fraction discarded!
System.out.println((double) total / count); // 2.5 - cast first
```
```console
2
2.5
```
*What just happened:* `5 / 2` is integer arithmetic, so the result is `2` with the `.5` silently dropped - no error, no warning, just a wrong number. Casting one operand to `double` forces *floating-point* division and gets `2.5`. The rule: for a fractional result, make at least one operand a `double` *before* the division happens. `(double)(total / count)` is too late - integer division already ran.

📝 **The other three, in one line each.** **`ConcurrentModificationException`** - removed from a list inside a for-each loop; use `list.removeIf(...)` or `Iterator.remove()`. **Shared mutable state** - handing out your internal list or array lets the receiver mutate it; return `List.copyOf(...)` instead. **Over-broad catch** - `catch (Exception e) {}` swallows the bug you need to see; catch the specific exception, never leave the block empty.

## Recap

1. **Program to interfaces** - declare `List`/`Map`, construct `ArrayList`/`HashMap`: narrow contracts, swappable implementations.
2. **Prefer immutability** - `final` fields, no setters, "change" returns a new object: thread-safe and bug-resistant by construction. (`final` on a reference freezes only the reference.)
3. **Return `Optional`, not `null`** - make "no value" visible in the type so callers can't forget it.
4. **Loop with enhanced for and streams**, and write real **`equals`/`hashCode`/`toString`** (matched pair for the first two) - records automate this later.
5. ⚠️ **The cheat-card** - `==` compares references (use `.equals()` for objects/strings); NPEs come from `null`; `Integer` caching makes `==` lie outside −128..127; `5/2` is `2`; shared references let others mutate your data; don't swallow exceptions.

That's idiomatic Java. You can now read other people's Java and write code that looks like it belongs - and you've met the traps before they meet you. Next: deep on **generics** - how `List<T>` really works, wildcards, and why the compiler sometimes argues about types.

## Quick check

Test yourself on the three traps that catch everyone:

```quiz
[
  {
    "q": "You have two String objects built with `new String(\"hi\")`. What do `a == b` and `a.equals(b)` return?",
    "choices": [
      "`a == b` is false (different objects), `a.equals(b)` is true (same characters)",
      "Both are true - strings always compare by value",
      "Both are false - the strings are stored separately",
      "`a == b` is true, `a.equals(b)` is false"
    ],
    "answer": 0,
    "explain": "`==` compares references, and `new String(...)` makes two distinct objects, so `a == b` is false. `.equals()` compares contents, so it's true. Always use `.equals()` for strings and objects."
  },
  {
    "q": "Why does `Integer a = 100, b = 100; a == b` print `true`, but `Integer c = 200, d = 200; c == d` print `false`?",
    "choices": [
      "Java caches Integer objects from −128 to 127, so 100 reuses one cached object while 200 creates two separate ones",
      "200 is too large to fit in an int, so it overflows",
      "`==` rounds large numbers differently",
      "It's undefined behavior and the result is random"
    ],
    "answer": 0,
    "explain": "The JVM caches small Integer objects (−128..127), so `a` and `b` are the same cached object and `==` is true. 200 is outside the cache, so `c` and `d` are separate objects and `==` is false. Use `.equals()` for wrapper objects."
  },
  {
    "q": "What does `5 / 2` evaluate to in Java, and how do you get `2.5`?",
    "choices": [
      "It's `2` (integer division discards the fraction); cast an operand to double, e.g. `5 / 2.0` or `(double) 5 / 2`",
      "It's `2.5` already - Java promotes to double automatically",
      "It's `3` - Java rounds to the nearest integer",
      "It throws an ArithmeticException for non-divisible numbers"
    ],
    "answer": 0,
    "explain": "Dividing two ints gives an int, dropping the remainder, so `5 / 2` is `2`. Force floating-point division by making at least one operand a double before the division runs."
  }
]
```


---

# Generics, Deep - Type Safety Without Duplication

Back in [Phase 3](03-collections.md) you wrote `List<String>` and moved on - strings in, strings out, no casting. That `<String>` was your first taste of generics.

The mental model for this phase: **generics let you write one piece of code that works for many types while the compiler still checks every type for you.** Before generics, you got reuse *or* safety, never both. Generics give you both, and pay for it with a single weird runtime tax called type erasure. Once you understand that trade, the confusing parts - wildcards, the compiler's errors - become consequences instead of arbitrary rules.

## Why generics exist - the pain they replaced

Before Java 5, collections held `Object` - the universal supertype every class extends. A list could hold anything, which sounds flexible until you try to *get something back out*.

```java
import java.util.ArrayList;
import java.util.List;

// Pre-generics style: a raw list holds Object.
List names = new ArrayList();
names.add("Ada");
names.add("Grace");
names.add(42);                  // oops - nothing stops this

// Getting a value back gives you an Object. You must cast.
String first = (String) names.get(0);   // fine
String third = (String) names.get(2);   // 42 is not a String...
```
```console
Exception in thread "main" java.lang.ClassCastException:
  class java.lang.Integer cannot be cast to class java.lang.String
```
*What just happened:* the raw `List` accepted a `String`, a `String`, and an `Integer` without complaint - to it, they're all just `Object`. The trouble surfaced at `names.get(2)`: the cast promised a `String`, the promise was a lie, and the JVM threw `ClassCastException` **at runtime**, after the bug shipped.

Now the same idea with generics:

```java
import java.util.ArrayList;
import java.util.List;

List<String> names = new ArrayList<>();
names.add("Ada");
names.add(42);                  // compile error - caught before you run
```
```console
error: incompatible types: int cannot be converted to String
        names.add(42);
                  ^
```
*What just happened:* `List<String>` told the compiler "this list holds strings, full stop." The bad `add(42)` was rejected **at compile time** - the program never even built. And since the compiler knows the element type, `names.get(0)` hands you a `String` directly, no cast required. 💡 A compile error is a gift - it's a runtime crash caught early, when it's cheap to fix.

## Generic methods and classes - `<T>` is a parameter for types

The `<String>` you've been writing is *using* a generic type. Now you'll *write* one. A **type parameter** is a placeholder for a type, written in angle brackets, filled in at the call site - exactly like a regular parameter is a placeholder for a value.

📝 **Type parameter** - a stand-in name (conventionally a single uppercase letter: `T` for "type," `E` for "element," `K`/`V` for "key"/"value") for a type the caller will supply, letting one definition serve every type.

A **generic method** declares its type parameter before the return type:

```java
import java.util.List;

// <T> says "this method introduces a type parameter named T."
// It works for a List of ANY type, returning that same type.
static <T> T first(List<T> list) {
    return list.get(0);
}

public static void main(String[] args) {
    List<String> words = List.of("alpha", "beta");
    List<Integer> nums = List.of(10, 20, 30);

    String w = first(words);    // T inferred as String
    int n = first(nums);        // T inferred as Integer
    System.out.println(w + " " + n);
}
```
```console
alpha 10
```
*What just happened:* `<T>` introduced a type parameter; `T` stands for whatever type the list holds, and the return type `T` means "same type I received." You never wrote `<String>` or `<Integer>` at the call sites - the compiler performed **type inference** off the argument, so `first(words)` made `T` be `String` and `w` needs no cast.

A **generic class** puts the type parameter on the class itself, so every instance is bound to a chosen type:

```java
// Box<T>: a container holding exactly one value of some type T.
class Box<T> {
    private final T value;

    Box(T value) {
        this.value = value;
    }

    T get() {
        return value;
    }
}

public static void main(String[] args) {
    Box<String> nameBox = new Box<>("Ada");   // T = String for this box
    Box<Integer> ageBox = new Box<>(36);      // T = Integer for this box

    String name = nameBox.get();              // no cast - compiler knows it's String
    System.out.println(name + " is " + ageBox.get());
}
```
*What just happened:* `class Box<T>` declared `T` once, and the whole class body - field, constructor, return type - could use it. `new Box<>("Ada")` (the `<>` "diamond") let the compiler infer `T` as `String`, so `nameBox.get()` returns a `String` directly; `ageBox` is a separate binding where `T` is `Integer`. `List<E>`, `Optional<T>`, and `Map<K, V>` are built the same way.

## Bounded type parameters - "any type, *as long as*"

A bare `<T>` means "literally any type" - too generous if your method needs to compare, add, or call a method on `T`, since not every type supports that. A **bounded type parameter** narrows the allowed types and unlocks the operations that bound guarantees.

📝 **Bound** - a constraint of the form `<T extends SomeType>` meaning "`T` must be `SomeType` or a subtype of it." It restricts which types the caller may use *and* lets the method body rely on everything `SomeType` provides. (`extends` here means "is-a," covering both extending classes and implementing interfaces.)

The classic case: finding the maximum of a list. To compare two `T`s, each must know how to compare *itself* - exactly what `Comparable` promises:

```java
import java.util.List;

// T must implement Comparable<T> - i.e. T values can be compared to each other.
static <T extends Comparable<T>> T max(List<T> list) {
    T biggest = list.get(0);
    for (T item : list) {
        if (item.compareTo(biggest) > 0) {   // legal ONLY because of the bound
            biggest = item;
        }
    }
    return biggest;
}

public static void main(String[] args) {
    System.out.println(max(List.of(3, 9, 2, 7)));        // Integer is Comparable
    System.out.println(max(List.of("pear", "fig", "kiwi"))); // String is Comparable
}
```
```console
9
pear
```
*What just happened:* `<T extends Comparable<T>>` reads "for any type `T` that can be compared to itself" - what makes `item.compareTo(biggest)` legal; without it, `T` might have no `compareTo`. `Integer` and `String` both implement `Comparable`, so both work. The bound does double duty: keeps out incomparable types, grants the method the right to compare.

A bound that isn't met stops the compiler cold:

```java
// A plain class that does NOT implement Comparable.
class Widget {}

static <T extends Number> double sum(List<T> list) {
    double total = 0;
    for (T item : list) {
        total += item.doubleValue();   // doubleValue() comes from Number
    }
    return total;
}

public static void main(String[] args) {
    sum(List.of(1, 2, 3));                 // fine: Integer extends Number
    sum(List.of(new Widget(), new Widget())); // bound violated
}
```
```console
error: method sum in class Demo cannot be applied to given types;
  required: List<T>
  found:    List<Widget>
  reason: inference variable T has incompatible bounds
    upper bounds: Number
    Widget is not within its upper bound
```
*What just happened:* `<T extends Number>` only admits `Number` and its subtypes (`Integer`, `Double`, `Long`, ...), making `item.doubleValue()` safe. `Integer` satisfies the bound, so the first call compiles; `Widget` doesn't extend `Number`, so the second is rejected before the program runs - "Widget is not within its upper bound."

## Wildcards and PECS - the part everyone trips on

⚠️ This section trips people up - not because the rule is hard, but because the *reason* behind it is unintuitive. We'll build the intuition, not just hand you the mnemonic.

You'd think `List<Integer>` is a kind of `List<Number>`, since an `Integer` *is* a `Number`. It is not:

```java
import java.util.List;

List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = ints;        // does NOT compile
```
```console
error: incompatible types: List<Integer> cannot be converted to List<Number>
```
*What just happened:* generics are **invariant** - `List<Integer>` and `List<Number>` are unrelated types even though `Integer` extends `Number`. If it were allowed, `nums.add(3.14)` would look fine (a `Double` is a `Number`) but would stuff a `Double` into a list the rest of your code believes holds only `Integer`s. Invariance stops that.

But invariance is sometimes too strict - a method that sums a list of numbers should accept `List<Integer>`, `List<Double>`, and `List<Long>` alike. **Wildcards** (`?`) restore that flexibility safely, in two mirror-image flavors.

📝 **Upper-bounded wildcard `? extends T`** - "some specific subtype of `T`, but I don't know which." You can *read* `T`s out of it. **Lower-bounded wildcard `? super T`** - "some specific supertype of `T`, but I don't know which." You can *write* `T`s into it.

`? extends T` makes a collection a **producer** - a source you read from:

```java
import java.util.List;

// Accepts a List of ANY subtype of Number - reads values out and sums them.
static double sumAll(List<? extends Number> list) {
    double total = 0;
    for (Number n : list) {          // reading as Number is always safe
        total += n.doubleValue();
    }
    return total;
}

public static void main(String[] args) {
    System.out.println(sumAll(List.of(1, 2, 3)));        // List<Integer> - OK!
    System.out.println(sumAll(List.of(1.5, 2.5)));       // List<Double>  - OK!
}
```
*What just happened:* `List<? extends Number>` means "a list of *some* unknown subtype of `Number`" - loose enough to accept both `List<Integer>` and `List<Double>`. Reading is safe: whatever the real element type is, it's *some* kind of `Number`, so pulling items out as `Number` always works.

You cannot *add* to a `? extends` collection - the famous head-scratcher:

```java
static void brokenAdd(List<? extends Number> list) {
    list.add(42);    // does NOT compile
}
```
```console
error: incompatible types: int cannot be converted to CAP#1
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Number from capture of ? extends Number
```
*What just happened:* the compiler refuses `list.add(42)` because it doesn't know the *real* element type - `list` might be a `List<Double>`, and an `Integer` would corrupt it. With `? extends`, the unknown type sits on the *output* side: take `Number`s out, put nothing in (except `null`) - read-only from the caller's view.

The mirror image is `? super T`, which makes a collection a **consumer** - a sink you write into:

```java
import java.util.ArrayList;
import java.util.List;

// Accepts any list that can HOLD an Integer: List<Integer>, List<Number>, List<Object>.
static void addThree(List<? super Integer> sink) {
    sink.add(1);     // safe: a List<Number> or List<Object> can hold an Integer
    sink.add(2);
    sink.add(3);
}

public static void main(String[] args) {
    List<Number> nums = new ArrayList<>();
    addThree(nums);              // works - Number is a supertype of Integer
    System.out.println(nums);
}
```
```console
[1, 2, 3]
```
*What just happened:* `List<? super Integer>` means "`Integer` or any supertype." Writing `Integer`s in is always safe - a `List<Number>` or `List<Object>` can hold one. Reading is restricted: the best the compiler can promise is `Object`, since the real list might hold any supertype. The unknown type sits on the *input* side: put `Integer`s in, can't pull specific types out.

That symmetry has a mnemonic the whole Java world uses:

📝 **PECS - Producer Extends, Consumer Super.** If a parameter *produces* values you'll read, use `? extends T`. If it *consumes* values you'll write, use `? super T`. (Pulling fruit *out* of a basket? The basket is a producer → `extends`. Dropping fruit *into* it? Consumer → `super`.)

💡 **Key point.** The wildcard puts the "I don't know exactly which type" on whichever side is *unsafe*. `? extends` doesn't know what you'd be adding, so it bans adds; `? super` doesn't know what you'd be reading, so it bans typed reads - the minimum restrictions that keep you from corrupting a collection whose real type you can't see.

## Type erasure - generics are a compile-time ghost

This explains a whole category of "wait, *why* can't I do that?" errors. All the type safety above happens at **compile time**. Once the compiler is satisfied, it throws the type information away - at runtime, the generics are gone.

📝 **Type erasure** - the compiler uses type parameters to check your code, then erases them, replacing each `T` with its bound (or `Object` if unbounded). The resulting bytecode has no generics at all - `List<String>` and `List<Integer>` compile down to the same plain `List`.

Watch the erasure with a runtime class check:

```java
import java.util.ArrayList;
import java.util.List;

public static void main(String[] args) {
    List<String> strings = new ArrayList<>();
    List<Integer> integers = new ArrayList<>();

    // At runtime, both are just "ArrayList" - the <String>/<Integer> is gone.
    System.out.println(strings.getClass() == integers.getClass());
}
```
```console
true
```
*What just happened:* `List<String>` and `List<Integer>` are different types to the *compiler*, which kept your strings and integers from mixing. But `getClass()` asks the *runtime*, which sees only `ArrayList` for both - the type parameter was erased after doing its job.

This single fact explains a cluster of restrictions that otherwise look arbitrary:

```java
class Box<T> {
    T makeOne() {
        return new T();      // does NOT compile
    }
}
```
```console
error: type parameter T cannot be instantiated directly
        return new T();
               ^
```
*What just happened:* you can't write `new T()` because at runtime there is no `T` - it's erased to `Object`, and the JVM wouldn't know which constructor to call. The same erasure forbids `new T[10]` (no generic array creation) and makes this illegal:

```java
class Printer {
    void print(List<String> items) {}    // erases to print(List)
    void print(List<Integer> items) {}   // ALSO erases to print(List) - clash!
}
```
```console
error: name clash: print(List<String>) and print(List<Integer>)
  have the same erasure
```
*What just happened:* overloading `print` on `List<String>` versus `List<Integer>` fails because both erase to `print(List)` - identical in bytecode though distinct in source. The "same erasure" error.

💡 **Key point.** Generics protect you at compile time, then vanish. Whenever the compiler complains about `T` at runtime-shaped operations (`new`, arrays, `instanceof`, overloading), ask "what is `T` after erasure?" The answer - usually `Object` - explains the restriction. Generics are a compile-time fiction the compiler maintains for *you*; the JVM never sees them.

## Recap

1. **Generics move type safety from runtime to compile time.** Before them, collections held `Object` and forced casts that blew up as `ClassCastException`; `List<String>` makes the same mistake a compile error instead.
2. **Type parameters** (`<T>`) are placeholders for types. A **generic method** declares `<T>` before its return type; a **generic class** (`Box<T>`) declares it on the class. The compiler **infers** the type at the call site.
3. **Bounded type parameters** (`<T extends Comparable<T>>`, `<T extends Number>`) restrict which types are allowed *and* unlock the operations that bound guarantees - the body can call `compareTo` or `doubleValue` only because the bound promised them.
4. **Wildcards** fix invariance safely. **PECS - Producer Extends, Consumer Super**: read from `? extends T` (no adding), write to `? super T` (no reading specific types). The unknown type goes on whichever side would be unsafe.
5. **Type erasure** means generics are compile-time only - `T` becomes its bound (usually `Object`) in the bytecode, so `List<String>` and `List<Integer>` are the same class at runtime. That's why you can't write `new T()`, `new T[]`, or overload on erased types.

You can now read other people's generic signatures, write your own, and decode the compiler's type complaints instead of guessing. Next: **lambdas and functional interfaces** - passing behavior around as values, which leans on generics constantly (`Function<T, R>`, `Predicate<T>`) to stay type-safe.

## Quick check

Test yourself on the three ideas that matter most - bounds, PECS, and erasure:

```quiz
[
  {
    "q": "Why does `static <T extends Comparable<T>> T max(List<T> list)` need the `extends Comparable<T>` bound?",
    "choices": [
      "Without it, the compiler can't guarantee values of T support compareTo, so the comparison in the body would be rejected",
      "It makes the method run faster by skipping runtime type checks",
      "It forces the list to be stored on the heap instead of the stack",
      "It's optional styling - the method compiles fine with a bare <T>"
    ],
    "answer": 0,
    "explain": "A bound both restricts which types T may be and unlocks that type's methods inside the body. Comparable provides compareTo; without the bound, T could be a non-comparable type, so item.compareTo(biggest) would be rejected."
  },
  {
    "q": "You have `List<? extends Number> list`. Why does `list.add(42)` fail to compile?",
    "choices": [
      "The real element type is unknown - it might be List<Double> - so adding an Integer could corrupt it; ? extends is read-only",
      "42 is too large to be a valid Number",
      "Wildcards make a list completely read-only, including reads",
      "You must call list.allowAdd() first to enable writing"
    ],
    "answer": 0,
    "explain": "With ? extends Number, the compiler only knows the list holds SOME unknown subtype of Number. It can't let you add anything (you might put an Integer into a List<Double>). You can read elements as Number, but not write - Producer Extends."
  },
  {
    "q": "At runtime, what does `new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass()` evaluate to, and why?",
    "choices": [
      "true - type erasure removes the type parameter, so both are just ArrayList at runtime",
      "false - they are permanently different types, even at runtime",
      "It throws a ClassCastException because the types don't match",
      "true - but only because both lists happen to be empty"
    ],
    "answer": 0,
    "explain": "Generics are erased after compilation: List<String> and List<Integer> both become plain ArrayList in the bytecode. getClass() asks the runtime, which sees only ArrayList for both - so the comparison is true. This same erasure is why you can't do new T() or overload on erased types."
  }
]
```


---

# Lambdas & Functional Interfaces - Functions as Values

Up to now, the unit of behavior in your Java has been the *method* - attached to an object, called by name. But sometimes you need to hand a *piece of behavior* to another method: "here's how to compare two things, you do the sorting." Before Java 8 that was painful enough that people avoided it; after, it became one line.

The mental model for this phase: **a lambda is a value - a chunk of behavior you can store in a variable, pass to a method, and call later.** Once that clicks, the Streams API next phase (`.filter(...).map(...)`) stops looking like magic and becomes the obvious consequence of "functions are values now."

## The problem - passing behavior the old way

Sometimes a method needs *behavior*, not just data. The classic case: sorting. `Collections.sort` knows how to sort, but not whether you want names sorted by length or alphabetically - that's *your* decision. So it asks for a `Comparator`: an object whose one job is to compare two items.

Before Java 8, the only way to supply that on the spot was an **anonymous inner class** - a whole class definition, inline, just to deliver one method.

```java
import java.util.*;

List<String> names = new ArrayList<>(List.of("Charlie", "Bo", "Alexandra"));

// Pre-Java-8: an anonymous class just to say "compare by length".
Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return Integer.compare(a.length(), b.length());
    }
});

System.out.println(names);
```
```console
[Bo, Charlie, Alexandra]
```
*What just happened:* the actual logic - "compare by length" - is one expression, `Integer.compare(a.length(), b.length())`. But delivering it took five lines of scaffolding: `new Comparator<String>() { @Override public int compare(...) { ... } }`. The class has no name and exists only to be passed once, yet you typed the type, signature, and braces by hand. The *intent* is buried under ceremony - the pain lambdas were invented to remove.

The same ceremony shows up everywhere behavior gets passed around - a `Runnable` for "code to run later," an event listener for "what to do on click." Boilerplate wrapping one small idea.

## Lambdas - anonymous functions, minus the ceremony

📝 **Lambda** - a compact, anonymous function written inline as a value: a parameter list, an arrow `->`, and a body. `(a, b) -> a + b` reads "given `a` and `b`, produce `a + b`." No name, not attached to a class - just behavior you can hand off.

A lambda is the anonymous-class boilerplate boiled down to its essence. Watch the same sort collapse:

```java
import java.util.*;

List<String> names = new ArrayList<>(List.of("Charlie", "Bo", "Alexandra"));

// Java 8+: the same comparator, as a lambda.
Collections.sort(names, (a, b) -> Integer.compare(a.length(), b.length()));

System.out.println(names);
```
```console
[Bo, Charlie, Alexandra]
```
*What just happened:* identical result, but the scaffolding is gone. `(a, b) -> Integer.compare(a.length(), b.length())` says exactly what the five-line version said - compare two strings' lengths - and nothing more: no `new Comparator<String>()`, no `@Override`, no method name, no braces. You didn't even write parameter types: the compiler already knows `Collections.sort` wants a `Comparator<String>`, so `a` and `b` must be strings, and it fills that in. The lambda is the behavior, noise removed.

A few syntax shapes you'll see, all the same idea:

```java
() -> 42                          // no parameters
x -> x * x                        // one parameter, parens optional
(int x, int y) -> x + y           // explicit types when you want them
(a, b) -> {                       // a block body, when you need statements
    int sum = a + b;
    return sum;                   // a block must return explicitly
}
```
*What just happened:* a single-expression body (`x * x`) is automatically the return value - no `return`, no semicolon. Use `{ }` braces and you're writing a normal method body that must `return` explicitly. With exactly one parameter you can drop the parentheses (`x -> ...`); with zero or more than one, they're required.

## Functional interfaces - what a lambda *actually is*

A lambda isn't attached to any class, so **what is its type?** Java is statically typed - every value has a type - so `(a, b) -> a + b` must *be* something.

📝 **Functional interface** - an interface with exactly **one abstract method** (a "SAM": *Single Abstract Method*). A lambda is an *instance* of one; its parameters and body become the implementation of that method.

That's the whole secret. When you wrote `(a, b) -> Integer.compare(...)`, the compiler saw `Collections.sort` wanted a `Comparator<String>` - one abstract method, `compare` - and treated your lambda *as* a `Comparator`, plugging its body in as that method. The lambda *implemented* the interface, invisibly.

This is **why a lambda always needs a target type.** Alone, `(a, b) -> a + b` is ambiguous - it could implement any two-argument interface. Java figures out which one from context: the parameter type, the variable it's assigned to, the enclosing method's return type. No target type, no lambda.

Prove it by writing your own functional interface:

```java
@FunctionalInterface
interface Calculator {
    int apply(int a, int b);          // exactly one abstract method
}

public class Demo {
    public static void main(String[] args) {
        Calculator add = (a, b) -> a + b;        // lambda IS a Calculator
        Calculator mul = (a, b) -> a * b;

        System.out.println(add.apply(3, 4));     // calls the lambda body
        System.out.println(mul.apply(3, 4));
    }
}
```
```console
7
12
```
*What just happened:* `Calculator` has one method, `apply`. The lambda `(a, b) -> a + b` becomes the *implementation*, so `add` is a real `Calculator` and `add.apply(3, 4)` runs the body, returning `7`. The variable's type (`Calculator`) is the target type telling the compiler what the lambda is. Swap the body to `a * b` for a different `Calculator` - no separate "lambda type" hiding anywhere.

💡 **`@FunctionalInterface`** is optional, but use it. It does nothing at runtime - it's a promise to the compiler: "exactly one abstract method." If someone later adds a second, the code won't compile, so you find out immediately instead of when a lambda mysteriously stops fitting.

⚠️ Don't confuse "one *abstract* method" with "one method total." A functional interface can have any number of `default`/`static` methods, since those have bodies and aren't abstract. `Comparator` is one (one abstract method, `compare`) despite carrying `default` helpers like `reversed()` and `thenComparing()`. Only the abstract count must be one.

## The built-in functional interfaces - the shared vocabulary

You *could* define a custom functional interface every time, but rarely need to. `java.util.function` ships a small set of general-purpose ones, and the whole modern Java ecosystem - Streams especially - speaks them. Learn these names and you can read any modern Java API.

Four core shapes, distinguished by one question: *does it take an input? does it return an output?*

📝 **`Function<T, R>`** - takes a `T`, returns an `R`. The general "transform one thing into another." Its method is `apply`.

📝 **`Predicate<T>`** - takes a `T`, returns a `boolean`. A yes/no test, the thing you filter with. Its method is `test`.

📝 **`Consumer<T>`** - takes a `T`, returns nothing. A side effect: print it, save it, log it. Its method is `accept`.

📝 **`Supplier<T>`** - takes nothing, returns a `T`. A source of values, often a deferred or lazy producer. Its method is `get`.

One line each makes the shapes concrete:

```java
import java.util.function.*;

Function<String, Integer> length = s -> s.length();      // String in, int out
Predicate<Integer> isEven       = n -> n % 2 == 0;       // int in, boolean out
Consumer<String> shout          = s -> System.out.println(s.toUpperCase());
Supplier<Double> random         = () -> Math.random();   // nothing in, double out

System.out.println(length.apply("hello"));   // 5
System.out.println(isEven.test(4));          // true
shout.accept("hi there");                    // HI THERE
System.out.println(random.get() < 1.0);      // true
```
```console
5
true
HI THERE
true
```
*What just happened:* four lambdas, four shapes. `length` *transforms* (Function: `apply`), `isEven` *tests* (Predicate: `test`), `shout` *has a side effect, no result* (Consumer: `accept`), `random` *produces from nothing* (Supplier: `get`). Each is a lambda assigned to the matching built-in type - no custom interface needed. The method name changes per type, but they're all "call the one abstract method."

There's also **`BiFunction<T, U, R>`** for two inputs, one output:

```java
import java.util.function.BiFunction;

BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
System.out.println(add.apply(3, 4));     // 7
```
```console
7
```
*What just happened:* this is the `Calculator` interface from earlier, except off-the-shelf - `BiFunction<Integer, Integer, Integer>` means "two things in, one out." (Relatives: `BiPredicate`, `UnaryOperator<T>` - a `Function` where input and output match - and `BinaryOperator<T>`.)

💡 **Why this matters.** These types are the *vocabulary* Streams and modern Java APIs speak. Seeing `.filter(Predicate)`, `.map(Function)`, `.forEach(Consumer)` next phase, you'll know exactly what each wants. Streams are just these four shapes wired into a pipeline.

## Method references - when a lambda just calls something

A lambda like `s -> s.length()` or `s -> System.out.println(s)` has one job: forward its argument to a method that already exists. That's common enough that Java gives it a shorter form: the **method reference**.

📝 **Method reference** - shorthand for a lambda that only calls an existing method. Written `Target::methodName`. `s -> s.toUpperCase()` becomes `String::toUpperCase`; `s -> System.out.println(s)` becomes `System.out::println`.

Four kinds, briefly:

- **Static method** - `Integer::parseInt` for `s -> Integer.parseInt(s)`.
- **Instance method of a particular object** - `System.out::println` for `s -> System.out.println(s)`.
- **Instance method of an arbitrary object of a type** - `String::toUpperCase` for `s -> s.toUpperCase()` (the receiver *becomes* the parameter).
- **Constructor** - `Account::new` for `() -> new Account()` (or with args, depending on the target type).

```java
import java.util.*;

List<String> names = new ArrayList<>(List.of("ada", "bo", "cleo"));

names.forEach(System.out::println);            // vs.  s -> System.out.println(s)
names.replaceAll(String::toUpperCase);         // vs.  s -> s.toUpperCase()
System.out.println(names);

List<String> nums = List.of("10", "20", "30");
int total = nums.stream().mapToInt(Integer::parseInt).sum();   // vs. s -> Integer.parseInt(s)
System.out.println(total);
```
```console
ada
bo
cleo
[ADA, BO, CLEO]
60
```
*What just happened:* `System.out::println` is the same `Consumer` as `s -> System.out.println(s)`, minus the obvious argument. `String::toUpperCase` is the "arbitrary object" kind - each list element becomes the receiver. `Integer::parseInt` is a static reference standing in for `s -> Integer.parseInt(s)`. When a lambda is *only* a call to an existing method, the method reference reads cleaner - the verb without the plumbing. If a lambda does more than one call, keep it a lambda.

⚠️ **Lambdas can only use local variables that are *effectively final*.** A lambda may read local variables from the enclosing scope, but only if never reassigned after being set (declared `final`, or just left alone - "effectively final"). Mutate one and the compiler refuses.

```java
int count = 0;
Runnable r = () -> System.out.println(count);   // OK: count is read-only here
// count = 5;                                    // would break it: count no longer effectively final
r.run();
```
```console
0
```
*What just happened:* the lambda *captured* `count` - carrying the value along so it can run later, possibly on another thread, long after this method returns. That only works if the value can't change out from under it, so Java requires captured locals to be effectively final. Uncomment `count = 5;` and the lambda stops compiling. (If you genuinely need to accumulate, mutate a field or an object the lambda holds, not the local itself.)

## Recap

1. **Lambdas solve the "passing behavior" problem.** Before Java 8 you handed behavior to a method via a verbose anonymous inner class (a `Comparator`, a `Runnable`); a lambda is the same thing, ceremony removed.
2. A **lambda** is a compact anonymous function - `(a, b) -> a + b` - stored, passed, and called. A single-expression body returns automatically; a `{ }` block must `return` explicitly.
3. **A lambda is an instance of a functional interface** - one abstract method (a SAM). That's *why* every lambda needs a target type: context decides which interface it implements. `@FunctionalInterface` enforces the one-method rule.
4. **The built-in functional interfaces are the shared vocabulary:** `Function<T,R>` transforms, `Predicate<T>` tests, `Consumer<T>` consumes with no result, `Supplier<T>` produces from nothing, `BiFunction` takes two inputs. Streams speak exactly these.
5. **Method references** (`String::toUpperCase`, `System.out::println`, `Account::new`) are shorthand for a lambda that only calls an existing method - four kinds, cleaner when the lambda is a single call.
6. ⚠️ Lambdas can only **capture effectively-final** local variables - the captured value must stay fixed for when the lambda runs later.

You can now treat behavior as a value. Next: the **Streams API**, where `Function`, `Predicate`, and `Consumer` chain into pipelines that filter, transform, and collect whole collections in a few readable lines.

## Quick check

Test yourself on the one insight that powers this whole phase - that a lambda *is* a functional interface:

```quiz
[
  {
    "q": "What is a lambda like `(a, b) -> a + b`, in Java's type system?",
    "choices": [
      "An instance of a functional interface - an interface with exactly one abstract method (a SAM)",
      "A brand-new primitive type built into the language",
      "A special object that has no type at all",
      "A renamed anonymous class that always implements Runnable"
    ],
    "answer": 0,
    "explain": "A lambda is an instance of a functional interface: the compiler uses the target type from context to decide which single-abstract-method interface the lambda implements, and the lambda's body becomes that one method. That's why a lambda always needs a target type."
  },
  {
    "q": "You need to pass behavior that takes a String and returns a boolean (a yes/no test). Which built-in functional interface fits?",
    "choices": [
      "Predicate<String> - it takes a T and returns a boolean, via its test method",
      "Function<String, String> - it transforms one String into another",
      "Consumer<String> - it takes a String and returns nothing",
      "Supplier<String> - it takes nothing and returns a String"
    ],
    "answer": 0,
    "explain": "Predicate<T> is the yes/no test: it takes a T and returns a boolean through test. Function transforms (returns a value of any type), Consumer returns nothing, and Supplier takes no input - none of those match 'String in, boolean out.'"
  },
  {
    "q": "Why must a local variable be 'effectively final' to be used inside a lambda?",
    "choices": [
      "The lambda captures the value to use later (possibly on another thread), so the value must stay fixed and not be reassigned",
      "Lambdas run faster when every variable is marked final",
      "Java forbids lambdas from reading any local variables at all",
      "It prevents the lambda from ever being garbage collected"
    ],
    "answer": 0,
    "explain": "A lambda may run long after the enclosing method returns, so it captures (carries along) the values it uses. For that to be safe, a captured local can't change out from under it - hence 'effectively final.' Reassigning the variable breaks compilation."
  }
]
```


---

# The Streams API - Declarative Data Pipelines

In [Phase 11](11-lambdas-and-functional-interfaces.md) you learned a lambda is a chunk of behavior you can pass around - `n -> n.length() > 3` is a value, not just code. The Streams API is where lambdas earn their keep: hand those behaviors to a pipeline and say "run this over the whole collection for me."

The mental shift: a loop is **imperative** - spell out every mechanical step: counter, bound check, grab, test, append. A stream is **declarative**: describe *what* you want and let Java handle the *how*. Same idea as Python's generator expressions and Rust's iterator chains - stop hand-driving the loop, start describing the transformation.

## The mental model - a pipeline, not a loop

A **Stream** is not a collection - it stores nothing. It's a *pipeline* describing a sequence of operations to run over a source of data: transform these, keep those, add them up. You build the pipeline by chaining methods, then ask for a result.

📝 **Stream** - a one-pass pipeline over a sequence of elements. Describes operations (filter, map, aggregate) to apply, holds no data of its own, can't be reused once consumed.

The fastest way to feel the difference: write the same task both ways. Say we have a list of numbers and want the squares of just the even ones.

```java
import java.util.List;
import java.util.ArrayList;

List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);

// Imperative: spell out every mechanical step.
List<Integer> result = new ArrayList<>();
for (int n : nums) {
    if (n % 2 == 0) {
        result.add(n * n);
    }
}
System.out.println(result);
```
```console
[4, 16, 36]
```
*What just happened:* this works, but most of it is *bookkeeping* - creating an empty list, loop scaffolding, manually appending. The intent ("keep evens, square them") is buried inside three lines of plumbing. Now as a stream:

```java
import java.util.List;
import java.util.stream.Collectors;

List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);

List<Integer> result = nums.stream()
        .filter(n -> n % 2 == 0)     // keep the evens
        .map(n -> n * n)             // square each one
        .collect(Collectors.toList());

System.out.println(result);
```
```console
[4, 16, 36]
```
*What just happened:* the intent is now the code. `filter` says "keep evens," `map` says "square them," `collect` says "gather into a list" - read top to bottom, it's almost an English sentence. No counter, no empty list to seed, no `.add()` to forget. That readability is the headline reason streams exist.

💡 **Key point.** A stream doesn't make a simple loop faster - for tiny tasks the loop may even be marginally quicker. The win is *clarity*: chained transformations read as a pipeline instead of a pile of mechanics. Reach for streams when filtering, mapping, and aggregating.

## Source → intermediate ops → terminal op

Every stream pipeline has the same three-part shape. Learn it and you can read any stream you'll meet.

📝 **The three parts.** **Source** - where elements come from (`list.stream()`, `Stream.of(...)`, `Arrays.stream(arr)`). **Intermediate operations** - `filter`, `map`, `sorted`, and friends; each returns *another stream* so they chain, and each is **lazy** (no work yet). **Terminal operation** - `collect`, `count`, `forEach`, `reduce`; returns a non-stream result and *triggers the whole pipeline to run*.

```mermaid
flowchart LR
  A["list.stream()<br/>SOURCE"] --> B["filter(...)<br/>intermediate (lazy)"]
  B --> C[".map(...)<br/>intermediate (lazy)"]
  C --> D["collect(...)<br/>TERMINAL - fires it all"]
```

The crucial, surprising part: **nothing runs until the terminal operation.** Intermediate ops just build up a description of work - the same laziness you saw in [Python's iterators](/guides/python-from-zero) and [Rust's iterator adapters](/guides/rust-from-zero): adapters describe, the consumer fires. Proof you can watch:

```java
import java.util.List;

List<String> names = List.of("ada", "bob", "cleo");

// No terminal operation - just intermediate ops.
names.stream()
     .filter(n -> {
         System.out.println("checking " + n);   // a side effect, to spy on it
         return n.length() == 3;
     });

System.out.println("--- pipeline built, but did it run? ---");
```
```console
--- pipeline built, but did it run? ---
```
*What just happened:* the `filter` body never printed a "checking" line - with no terminal operation, the pipeline was built and thrown away without running; the lambda was *never called*. Add a terminal op (`.count()`, `.collect(...)`, `.forEach(...)`) and the lines appear.

⚠️ **Gotcha - a stream with no terminal op does nothing.** The number-one stream surprise. If your filtering or mapping seems ignored, check that you ended with a terminal operation - a bare `list.stream().filter(...).map(...);` is a recipe nobody cooked. (Related trap: a stream is **single-use**. Once a terminal op consumes it, calling another operation on the same stream throws `IllegalStateException`. Build a fresh stream from the source each time.)

## Common operations - building the pipeline

The operations you'll reach for daily. Most are intermediate (return a stream); `reduce` and `collect` are terminal.

- **`filter(predicate)`** - keep only elements that pass the test.
- **`map(function)`** - transform each element into something else (often a different type).
- **`sorted()`** / **`sorted(comparator)`** - order the elements.
- **`distinct()`** - drop duplicates (uses `equals`).
- **`limit(n)`** - keep at most the first `n` elements, then stop.
- **`reduce(...)`** - collapse the whole stream into a single value (sum, product, concatenation).

A realistic chain: raw names, keep only the "adult" ones (length standing in for some real condition), uppercase, sort, collect as a list.

```java
import java.util.List;
import java.util.stream.Collectors;

List<String> raw = List.of("bob", "ada", "cleo", "al", "bob");

List<String> cleaned = raw.stream()
        .filter(name -> name.length() >= 3)   // drop the too-short "al"
        .distinct()                           // collapse the duplicate "bob"
        .map(String::toUpperCase)             // shout each name
        .sorted()                             // alphabetical order
        .collect(Collectors.toList());

System.out.println(cleaned);
```
```console
[ADA, BOB, CLEO]
```
*What just happened:* read the chain as a sentence. `filter` removed `"al"` (too short); `distinct` collapsed the two `"bob"`s into one; `map` uppercased each survivor (`String::toUpperCase` is a method reference from Phase 11); `sorted` alphabetized them; `collect` gathered the result into a `List`. Five clear steps, each line exactly one transformation.

`reduce`, the terminal op that folds a stream down to one value:

```java
import java.util.List;

List<Integer> prices = List.of(10, 25, 5, 40);

int total = prices.stream()
        .reduce(0, (running, price) -> running + price);   // start at 0, keep adding

System.out.println("total: " + total);
```
```console
total: 80
```
*What just happened:* `reduce` takes a starting value (`0`) and a function combining the running result with the next element: `0+10`, `10+25`, `35+5`, `40+40`, arriving at `80` - the general shape of "boil a sequence down to one answer." For common cases (sum, average, count) reach for a purpose-built collector or `IntStream.sum()` instead; `reduce` is the engine underneath them all.

## Collectors - reshaping a stream into a result

A stream produces a sequence; eventually you want it back as something storable - a `List`, `Map`, count, or joined string. That's the job of **collectors**, used with the `collect(...)` terminal op.

💡 **Key point.** Think of `Collectors` as the toolbox for the *last* step: "I have a stream of elements - reshape them into a List, a Map, or a summary."

The everyday collectors:

```java
import java.util.List;
import java.util.stream.Collectors;

List<String> words = List.of("apple", "banana", "cherry");

List<String> asList = words.stream()
        .map(String::toUpperCase)
        .collect(Collectors.toList());                 // -> a List

String joined = words.stream()
        .collect(Collectors.joining(", ", "[", "]"));  // glue with separators

System.out.println(asList);
System.out.println(joined);
```
```console
[APPLE, BANANA, CHERRY]
[apple, banana, cherry]
```
*What just happened:* `Collectors.toList()` gathered the mapped elements into a `List`. `Collectors.joining(", ", "[", "]")` stitched the strings together with a separator, prefix, and suffix - cleaner than building a `StringBuilder` by hand. (Modern Java also offers `.toList()` directly on the stream as a shortcut.)

Now the one that changes how you think: **`groupingBy`**. It takes a function producing a *key* for each element and hands back a `Map` where each key points to the list of elements sharing it - the streaming equivalent of SQL's "GROUP BY."

```java
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

record Person(String name, String city) {}

List<Person> people = List.of(
        new Person("Ada", "London"),
        new Person("Bob", "Paris"),
        new Person("Cleo", "London"),
        new Person("Dan", "Paris")
);

Map<String, List<Person>> byCity = people.stream()
        .collect(Collectors.groupingBy(Person::city));

System.out.println(byCity.get("London"));
```
```console
[Person[name=Ada, city=London], Person[name=Cleo, city=London]]
```
*What just happened:* `groupingBy(Person::city)` called `.city()` on each person for a key and bucketed everyone into a `Map<String, List<Person>>` keyed by city - Ada and Cleo in `"London"`, Bob and Dan in `"Paris"`. By hand this means a map, a loop, `computeIfAbsent`, and appending - a dozen lines collapsed into one. Nest a *second* collector to summarize each group:

```java
import java.util.Map;
import java.util.stream.Collectors;

// ...same `people` list as above...

Map<String, Long> countByCity = people.stream()
        .collect(Collectors.groupingBy(Person::city, Collectors.counting()));

System.out.println(countByCity);
```
```console
{London=2, Paris=2}
```
*What just happened:* the second argument, `Collectors.counting()`, tells `groupingBy` not to collect the people themselves but to *count* them per group - `{London=2, Paris=2}`. Other downstream collectors work the same way: `Collectors.toMap` builds a key→value map directly, `Collectors.mapping(...)` transforms group members before collecting. One collector can feed another.

## Parallel streams & when to use streams

This looks like free speed and is mostly a trap if used carelessly. Swap `.stream()` for `.parallelStream()` (or call `.parallel()` on an existing stream) and Java splits the work across multiple CPU cores using the common fork/join pool.

```java
import java.util.stream.IntStream;

long count = IntStream.rangeClosed(1, 1_000_000)
        .parallel()
        .filter(n -> n % 7 == 0)
        .count();

System.out.println(count);
```
```console
142857
```
*What just happened:* filtering a million numbers got chopped into chunks, run on several cores at once, then combined - a genuine speedup on a multi-core machine for big, CPU-bound, independent computations.

⚠️ **Parallel is not free, and can make things slower or wrong.** **(1)** Splitting, scheduling, and merging have overhead - for small collections or cheap per-element work, parallel is *slower* than plain `.stream()`; it only pays off for large data with real per-element cost. **(2)** Side effects in parallel are dangerous: a lambda mutating a shared `ArrayList` or counter from multiple threads is a data race waiting to corrupt results or throw - keep stream operations *pure*. **(3)** Order-dependent operations get more expensive or behave differently. Measure before you parallelize.

💡 **When should you use streams at all?** Reach for one when expressing a *transformation* - filter, map, group, aggregate - since the pipeline reads cleanly. Stick with a plain `for` loop when the logic is simple, when you need an early `break` streams make awkward, when mutating external state, or when a stream would genuinely be *harder* to read. A clear loop beats a contorted stream - don't force it.

## Recap

1. A **Stream** is a declarative pipeline over a sequence - you describe *what* to do (filter, map, aggregate), not the loop mechanics. Stores no data, single-use.
2. Every pipeline is **source → intermediate ops → terminal op**. Intermediate ops (`filter`, `map`, `sorted`, `distinct`, `limit`) are **lazy** and chainable; the terminal op (`collect`, `count`, `forEach`, `reduce`) fires the whole thing.
3. ⚠️ **Nothing runs without a terminal operation** - the most common stream mistake.
4. **Collectors** reshape a stream into a result: `toList`, `joining`, `toMap`, and especially **`groupingBy`** (with downstream collectors like `counting`) for bucketing elements into a `Map`.
5. **`parallelStream()`** splits work across cores - only helps for large, CPU-bound, side-effect-free work. Measure first; pure operations only.
6. Use streams for readable transformations; a plain loop is fine for simple cases. Don't force it.

You can now read and write the pipeline style that dominates modern Java codebases. Next: **records and sealed types** - the features that make the immutable, intent-revealing data classes from the idioms phase a single line of code.

## Quick check

Test yourself on the three ideas that make streams tick - the pipeline shape, laziness, and grouping:

```quiz
[
  {
    "q": "You write `list.stream().filter(x -> x > 0).map(x -> x * 2);` and nothing seems to happen. Why?",
    "choices": [
      "There's no terminal operation - `filter` and `map` are lazy and do no work until a terminal op like `collect`, `count`, or `forEach` runs",
      "`filter` and `map` can't be used together in the same pipeline",
      "Streams only run if you call `.start()` on them",
      "The lambda syntax is invalid, so the pipeline is silently skipped"
    ],
    "answer": 0,
    "explain": "Intermediate operations (`filter`, `map`) are lazy: they only build up a description of the work. Nothing actually runs until a terminal operation triggers the pipeline. With no terminal op, you've built a recipe and thrown it away."
  },
  {
    "q": "What does `Collectors.groupingBy(Person::city)` produce when collected from a stream of people?",
    "choices": [
      "A `Map<String, List<Person>>` where each city key maps to the list of people in that city",
      "A flat `List<Person>` sorted by city name",
      "A single `String` of all the city names joined together",
      "A count of how many distinct cities exist"
    ],
    "answer": 0,
    "explain": "`groupingBy(Person::city)` calls `.city()` on each element to get a key and buckets the elements into a `Map` from key to the `List` of elements sharing it. Add a downstream collector like `Collectors.counting()` to summarize each group instead of listing its members."
  },
  {
    "q": "When is switching `.stream()` to `.parallelStream()` actually a good idea?",
    "choices": [
      "For large, CPU-bound work with no shared mutable state - and only after measuring, since splitting has overhead",
      "Always - parallel streams are strictly faster than sequential ones",
      "For tiny collections, where the overhead is negligible",
      "When your lambdas mutate a shared list, to speed up the writes"
    ],
    "answer": 0,
    "explain": "Parallelism pays off only for big, independent, CPU-bound work, and the split/merge overhead can make small or cheap tasks slower. Side effects on shared state in parallel are a data race - keep operations pure, and measure before reaching for `.parallel()`."
  }
]
```


---

# Records, Sealed Types & Modern Java - Less Boilerplate, More Safety

Java has a reputation: a language where a simple thing takes fifteen lines. Back in
[Phase 5](05-classes-and-objects.md) you wrote an `Account` class with private fields, a constructor,
getters, and a hand-written `toString`/`equals`/`hashCode` trio - for *one* data type. Multiply that across
a real codebase and "ceremony" starts to feel like an insult.

Modern Java is quieter and sharper than its reputation. A whole wave of features exists for one reason: let
you say the common thing concisely, and let the *compiler* catch mistakes it used to wave through. The
mental model for this phase: **take a tired, verbose pattern and replace it with a tight, safer one** -
repeated five times.

## Records - the data class in one line

📝 A **record** is an immutable data carrier: a class whose entire job is to hold a few values. You declare
the fields it carries, and the compiler generates everything else - the constructor, an accessor per field,
and correct `equals`, `hashCode`, and `toString`.

Here's roughly the `Account`-class amount of code for a simple two-value point, the old way:

```java
public final class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int x() { return x; }
    public int y() { return y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point)) return false;
        Point other = (Point) o;
        return x == other.x && y == other.y;
    }

    @Override
    public int hashCode() {
        return java.util.Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point[x=" + x + ", y=" + y + "]";
    }
}
```

*What just happened:* Thirty-odd lines, none interesting - just the mechanical bookkeeping the
[Phase 5 trio](05-classes-and-objects.md) warned you to get right. Worse, every line risks a bug: forget a
field in `equals`, mismatch `hashCode`, and your objects misbehave in a `HashSet`.

The same type as a record:

```java
public record Point(int x, int y) {}
```

*What just happened:* That one line generates *all* of the above - a constructor taking `x` and `y`,
accessors `x()`/`y()`, and correct `equals`/`hashCode`/`toString` covering both fields. The fields are
`private final`, so a `Point` is immutable. (Accessors use the field name directly, not `getX()`/`getY()`.)

```java
public class Main {
    public static void main(String[] args) {
        Point a = new Point(1, 2);
        Point b = new Point(1, 2);

        System.out.println(a);            // generated toString
        System.out.println(a.x());        // generated accessor
        System.out.println(a.equals(b));  // generated value equality
    }
}
```
```console
$ java Main.java
Point[x=1, y=2]
1
true
```

*What just happened:* Printing `a` gave a readable `Point[x=1, y=2]` for free, `a.x()` read the first field,
and `a.equals(b)` returned `true` because the generated `equals` compares values - the thing you had to
hand-write before. Two separate objects with the same data are equal, as a value type should be.

💡 **When to reach for a record.** Use one whenever the type's purpose is *to carry data*: DTOs (a JSON
request shape, a query row), value objects (`Money`, `Coordinate`), or a method returning two things at
once. A class that's all fields and getters almost certainly wants to be a record.

⚠️ **Need validation? Use a compact constructor.** A record isn't a dumb tuple. Write a *compact constructor*
(the record name, no parameter list) to validate before the fields are assigned:

```java
public record Point(int x, int y) {
    public Point {                        // compact constructor - no parameters listed
        if (x < 0 || y < 0) {
            throw new IllegalArgumentException("coordinates must be non-negative");
        }
        // no explicit assignment needed - the fields are set for you afterward
    }
}
```

*What just happened:* The compact constructor runs your check, then Java assigns `x` and `y` automatically -
you don't write `this.x = x`. Now `new Point(-1, 0)` throws instead of quietly creating a nonsense point, so
a record stays immutable *and* always valid.

## `Optional` - a box that might be empty, instead of `null`

You met `null` and its favorite crime, `NullPointerException`, back in [Phase 7](07-errors-and-io.md). The
modern reply to "this might not have a value" is `Optional` - and the point isn't magic, it's *being upfront about it*.

`Optional<User>` is a small wrapper holding either one `User` or nothing. A method returning
`Optional<User>` tells callers, right in the signature, "this might come back empty - deal with it."
Compare a bare `User` that's *sometimes* `null`: nothing warns you, you forget the check, and the NPE
finds you in production.

```java
import java.util.Optional;

public class Users {
    // Old way: returns User, or null if not found - the caller can't tell.
    // New way: the Optional makes "might be missing" part of the contract.
    static Optional<String> findName(int id) {
        if (id == 1) return Optional.of("Ada");
        return Optional.empty();          // plain "nothing here"
    }

    public static void main(String[] args) {
        String found   = findName(1).map(String::toUpperCase).orElse("(unknown)");
        String missing = findName(99).map(String::toUpperCase).orElse("(unknown)");

        System.out.println(found);
        System.out.println(missing);

        findName(1).ifPresent(name -> System.out.println("present: " + name));
    }
}
```
```console
$ java Users.java
ADA
(unknown)
present: Ada
```

*What just happened:* `findName` returns an `Optional<String>` instead of a possibly-`null` string. `map`
transforms the value *if present*, `orElse` supplies a fallback, `ifPresent` runs code only when a value
exists. The id-99 lookup flowed through with no explicit `if (x == null)` and no risk of an NPE.

⚠️ **Don't over-`Optional`.** It's for *return values* that may legitimately be absent - not for fields (it
bloats objects and breaks serialization) and not for method parameters (overload the method or accept the
plain type instead). Never call `.get()` without checking - that's `null` with extra steps. Use `Optional`
to make a *missing result* impossible to ignore; don't sprinkle it everywhere.

## Switch expressions - `switch` that returns a value

The old C-style `switch` was a minefield: a colon per `case`, a `break` you had to remember or fall through
by accident, and no way to produce a value directly. Modern Java turns `switch` into an **expression** that
hands back a value, with no fall-through.

A switch expression uses the arrow form `case X -> result;`, evaluates to a value you assign directly, and
is *exhaustive* - for an enum, the compiler checks you've covered every case. No `break`; each arrow handles
one case. A branch needing more than one line wraps in braces and uses `yield`.

```java
public class Main {
    enum Day { MON, TUE, WED, THU, FRI, SAT, SUN }

    static String kind(Day day) {
        return switch (day) {                       // switch as an expression
            case SAT, SUN -> "weekend";             // group cases with a comma
            case MON, TUE, WED, THU, FRI -> {       // a block branch...
                String note = "five of these";
                yield "weekday (" + note + ")";     // ...uses yield to produce its value
            }
        };
    }

    public static void main(String[] args) {
        System.out.println(kind(Day.SAT));
        System.out.println(kind(Day.MON));
    }
}
```
```console
$ java Main.java
weekend
weekday (five of these)
```

*What just happened:* The whole `switch` *evaluated to* a string returned directly - no temp variable, no
`break`, no fall-through. `SAT, SUN` were grouped on one arrow; the multi-line branch used `yield` to
produce its value. Covering every `Day` lets the compiler accept it with no `default`; leave a case out and
it refuses to compile - a missing case is an error, not a silent runtime bug.

## Pattern matching - test the type and bind in one move

A tired old ritual: check a type with `instanceof`, then cast to that exact type on the next line to use it.
Two lines saying the same thing, and a chance to typo the cast. Pattern matching fuses them.

`if (obj instanceof String s)` tests *and*, when the test passes, binds the value to a new variable `s` of
that type - already cast, ready to use. The same pattern works inside `switch`, and can even *deconstruct*
a record into its components.

```java
public class Main {
    static String describe(Object obj) {
        // Old way: if (obj instanceof String) { String s = (String) obj; ... }
        if (obj instanceof String s) {
            return "string of length " + s.length();   // s is already a String
        }
        return switch (obj) {
            case Integer i -> "int doubled = " + (i * 2);  // i is bound as an Integer
            case Point(int x, int y) -> "point at " + x + "," + y;  // record deconstruction
            default -> "something else";
        };
    }

    record Point(int x, int y) {}

    public static void main(String[] args) {
        System.out.println(describe("hello"));
        System.out.println(describe(21));
        System.out.println(describe(new Point(3, 4)));
        System.out.println(describe(3.14));
    }
}
```
```console
$ java Main.java
string of length 5
int doubled = 42
point at 3,4
something else
```

*What just happened:* `obj instanceof String s` checked the type and bound `s` in one step, so `s.length()`
worked immediately - no separate cast. Inside the `switch`, `case Integer i ->` did the same per branch, and
`case Point(int x, int y) ->` went further: it matched a `Point` *and* pulled its two components straight
into `x` and `y`. That last move - **record deconstruction** - is where records and pattern matching team up.

## Sealed types - a fixed, compiler-checked set of cases

Sometimes a type needs a *known, fixed* set of implementations - a `Shape` is a `Circle` or a `Square`,
nothing else, ever. Plain interfaces can't express that: anyone can write a new implementor, so the compiler
can never be sure a `switch` handled them all. Sealed types fix this.

📝 A **sealed** type uses `sealed ... permits` to name the *only* types allowed to extend or implement it:
`sealed interface Shape permits Circle, Square`. Nobody outside that list can join the family. The payoff:
`switch` over it with patterns - since the compiler knows the complete set, it can verify the switch is
**exhaustive**, with *no `default` needed*. Miss a case and you get a compile error, not a runtime surprise.

```java
public class Main {
    sealed interface Shape permits Circle, Square {}
    record Circle(double radius) implements Shape {}
    record Square(double side) implements Shape {}

    static double area(Shape s) {
        return switch (s) {
            case Circle(double r) -> Math.PI * r * r;   // deconstruct the record
            case Square(double side) -> side * side;
            // no default - the compiler knows Circle and Square are ALL the cases
        };
    }

    public static void main(String[] args) {
        System.out.printf("%.2f%n", area(new Circle(2)));
        System.out.printf("%.2f%n", area(new Square(3)));
    }
}
```
```console
$ java Main.java
12.57
9.00
```

*What just happened:* `Shape` permits exactly `Circle` and `Square`, so the `switch` covering both is
provably complete - satisfied without a `default`. Add a `Triangle` to `permits` six months from now, and
every `switch` over `Shape` that missed it *stops compiling*, pointing at each place needing an update. The
compiler becomes your checklist instead of a bug report.

💡 **Sealed + records + switch = a "one of a fixed set" type.** Together these give Java what other
languages call *discriminated unions* or *sum types*: a value that's exactly one of a closed list of
shapes, each carrying its own data, handled by a switch the compiler guarantees is total - the cleanest way
to model "a result is either a `Success(value)` or a `Failure(reason)`," with exhaustiveness checking free.

## Recap

1. A **record** (`record Point(int x, int y) {}`) collapses a whole data class - constructor, accessors,
   `equals`/`hashCode`/`toString` - into one immutable line. Use it for DTOs and value objects; add a
   **compact constructor** for validation.
2. **`Optional<T>`** makes "might be absent" part of a method's return type, so callers can't forget the
   case. Use `map`/`orElse`/`ifPresent`; ⚠️ don't put it on fields or parameters.
3. **Switch expressions** return a value with the `case X -> ...` arrow form - no `break`, no fall-through,
   `yield` for multi-line branches, **exhaustiveness** checked for enums and sealed types.
4. **Pattern matching** fuses the type test and the cast: `if (obj instanceof String s)` binds `s` ready to
   use, works in `switch` (`case Integer i ->`), and can **deconstruct records** (`case Point(int x, int y)`).
5. **Sealed types** (`sealed interface Shape permits Circle, Square`) fix the set of implementations, letting
   the compiler prove a pattern `switch` is exhaustive - a missing case is a compile error.
6. Together - **sealed + records + switch** - these are Java's answer to discriminated unions: "one of a
   fixed set," handled totally, checked by the compiler.

You now write Java the way modern Java wants to be written: less ceremony, more meaning, and a compiler
that catches mistakes the old style let slip. Next: leaving single-threaded code behind for the hard,
fascinating world of doing several things at once.

## Quick check

Test yourself on the ideas that separate old Java from modern Java:

```quiz
[
  {
    "q": "What does `public record Point(int x, int y) {}` generate for you?",
    "choices": [
      "A constructor, accessors `x()` and `y()`, and correct `equals`, `hashCode`, and `toString` - for an immutable type",
      "Only an empty class with two public mutable fields",
      "Getters named `getX()` and `getY()`, but no `equals` or `hashCode`",
      "Nothing - `record` is just a comment-style keyword"
    ],
    "answer": 0,
    "explain": "A record is an immutable data carrier. The compiler generates the canonical constructor, an accessor per field (named after the field, like `x()`), and value-based `equals`/`hashCode`/`toString` - replacing the ~40 lines you'd otherwise hand-write."
  },
  {
    "q": "Why can a pattern-matching `switch` over a sealed type skip the `default` branch?",
    "choices": [
      "Because `sealed ... permits` fixes the complete set of subtypes, so the compiler can verify every case is covered",
      "Because switch expressions never require a default under any circumstances",
      "Because `default` is forbidden inside any switch expression",
      "Because sealed types disable compile-time checking entirely"
    ],
    "answer": 0,
    "explain": "A sealed type names all its permitted implementations, so the compiler knows the full set of cases. When your switch covers them all, it's provably exhaustive - no `default` needed, and missing a case becomes a compile error."
  },
  {
    "q": "What is the right use of `Optional<User>`?",
    "choices": [
      "As a method return type that signals the result may legitimately be absent, forcing callers to handle the empty case",
      "As the type of every field in a class, to avoid ever storing null",
      "As a method parameter type so callers can pass nothing",
      "As a faster replacement for a regular `User` object"
    ],
    "answer": 0,
    "explain": "`Optional` exists to make 'this might be missing' explicit in a return type, so callers can't silently forget the check. It's not meant for fields or parameters - using it there adds overhead and awkwardness without the payoff."
  }
]
```


---

# Concurrency & Threads - Doing Many Things at Once, Safely

Every program you've written so far has done one thing at a time: line runs, finishes, next line runs. Concurrency breaks that: you ask the machine to run several paths of execution *simultaneously*, so a server can handle a thousand requests at once, or an app can download a file without freezing the UI.

The plain framing: **starting threads is easy; sharing data without corrupting it is the hard part.** A thread alone is tame. Two threads touching the same variable with no coordination is where careers' worth of subtle, intermittent, impossible-to-reproduce bugs come from. We'll make threads, learn the danger they create, then spend most of our time on the tools that tame it.

The arc: raw `Thread`s (the bricks), race conditions (the disease), `synchronized` and the memory model (the cure), `java.util.concurrent` (the tools to actually reach for), and virtual threads (the modern leap that makes blocking code scale).

## Threads - an independent path of execution

📝 **A thread is an independent path of execution within your program.** Your program already has one - the "main" thread running `main()`. A new thread is a second worker running its own code at the same time, sharing the first's memory. The OS juggles them across CPU cores (and, when threads outnumber cores, by rapidly switching between them).

You create one by giving it work: a `Runnable` - an object with a single `run()` method - written cleanest as a lambda ([Phase 11](11-lambdas-and-functional-interfaces.md)):

```java
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            System.out.println("worker thread: " + Thread.currentThread().getName());
        });

        worker.start();          // spawns a NEW thread, runs the lambda there
        System.out.println("main thread: " + Thread.currentThread().getName());

        worker.join();           // wait for the worker to finish before exiting
    }
}
```
```console
$ java Main.java
main thread: main
worker thread: Thread-0
```
*What just happened:* `new Thread(() -> ...)` wrapped your lambda as the thread's job but did *not* run it yet. `worker.start()` asked the OS for a new thread and ran the lambda there, concurrently with `main`. The two `println`s race - `main` printed first here, but the order can flip next run, since nobody coordinated them. `worker.join()` blocks main until the worker finishes, so the program doesn't exit out from under it.

⚠️ **The bug that bites everyone once: calling `run()` instead of `start()`.** They look interchangeable. They aren't.

```java
Thread worker = new Thread(() -> {
    System.out.println("on: " + Thread.currentThread().getName());
});

worker.run();    // WRONG - runs the lambda on the CURRENT thread, no new thread at all
worker.start();  // RIGHT - spawns a new thread
```
```console
$ java Main.java
on: main
on: Thread-0
```
*What just happened:* `run()` is a normal method call - it executes the lambda on whatever thread called it (`main`). No concurrency happens. Only `start()` asks the OS for a fresh thread. If "concurrent" code mysteriously runs perfectly in order, single-file, check whether someone called `run()` - the silent no-op of Java threading.

## The danger - shared mutable state & race conditions

The moment two threads read *and* write the same data, you have a potential **race condition** - the central villain of all concurrency.

📝 **A race condition is a bug where the correctness of your program depends on the unpredictable timing of threads.** The result changes depending on which thread gets there first - the program might work a million times and fail on the million-and-first, depending on how the OS scheduled things that instant.

The classic demonstration: two threads incrementing the same counter. You'd expect 200,000; you won't get it.

```java
public class Main {
    static int counter = 0;   // shared, mutable - the danger zone

    public static void main(String[] args) throws InterruptedException {
        Runnable job = () -> {
            for (int i = 0; i < 100_000; i++) {
                counter++;          // looks atomic. is not.
            }
        };

        Thread t1 = new Thread(job);
        Thread t2 = new Thread(job);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("expected 200000, got: " + counter);
    }
}
```
```console
$ java Main.java
expected 200000, got: 143281
```
*What just happened:* `counter++` looks like one step, but it's three: **read**, **add** one, **write** back. Two threads can both read `5`, both compute `6`, both write `6` - two increments, one gain. That lost update, repeated tens of thousands of times, is why we landed at `143281` instead of `200000`. ⚠️ Run it yourself and you'll get a *different* wrong number each time - that non-determinism is what makes race conditions so maddening to track down.

Threads are cheap. *Sharing mutable state correctly* is the expensive, careful work, and everything below exists to make it safe.

## `synchronized` & the memory model

The fix is **mutual exclusion**: guarantee only one thread at a time can run the read-add-write sequence. Java's built-in tool is the `synchronized` keyword.

📝 **`synchronized` marks a block (or method) as a critical section guarded by a lock.** Every Java object has an invisible lock (a "monitor"). A thread entering a `synchronized` block must acquire that lock; if another holds it, the newcomer waits. Only one thread runs the protected code at a time, so the read-add-write can't be interrupted halfway.

```java
public class Main {
    static int counter = 0;
    static final Object lock = new Object();   // the thing we lock on

    public static void main(String[] args) throws InterruptedException {
        Runnable job = () -> {
            for (int i = 0; i < 100_000; i++) {
                synchronized (lock) {           // one thread at a time, in here
                    counter++;
                }
            }
        };

        Thread t1 = new Thread(job);
        Thread t2 = new Thread(job);
        t1.start(); t2.start();
        t1.join();  t2.join();

        System.out.println("expected 200000, got: " + counter);
    }
}
```
```console
$ java Main.java
expected 200000, got: 200000
```
*What just happened:* Each thread had to grab `lock` before touching `counter`, so the read-add-write sequence ran start-to-finish without the other sneaking in - every one of the 200,000 increments landed. The trade-off is real: threads now take turns through that block instead of running in parallel, so heavily-contended locks cost speed. The art is locking the *smallest* section that keeps you correct.

💡 **The memory model, in one paragraph.** A second, sneakier danger beyond lost updates: **visibility**. The JVM and CPU can cache variables in registers and reorder instructions for speed, so a write one thread makes to a shared variable might *never become visible* to another - it could spin forever reading a stale cached value. The **Java Memory Model (JMM)** defines when one thread's writes are guaranteed visible to another, via **happens-before**: releasing a lock happens-before another thread acquiring that same lock, so everything written inside a `synchronized` block is visible to the next thread entering it. That's why `synchronized` fixes *both* atomicity and visibility at once.

📝 **`volatile` handles visibility alone.** Marking a field `volatile` guarantees every read sees the most recent write from any thread - no stale caches, no reordering past it. But it does **not** give atomicity: a `volatile` counter still loses updates under `counter++`, since read-add-write is still three steps. ⚠️ Rule of thumb: `volatile` for a simple flag one thread sets and another reads (like `boolean running`); `synchronized` (or atomics below) the moment a thread needs to *read-then-write* shared state.

## Higher-level concurrency - prefer it

The most important practical advice in this phase: **most of the time, you should not be writing `new Thread(...)` or `synchronized` by hand at all.** Hand-managing threads is error-prone and wasteful - every `new Thread` is a real OS thread costing memory and startup time, and writing your own locking is how the subtle bugs above creep in. `java.util.concurrent` gives you battle-tested tools that handle all of it.

💡 **The default move: reach for `java.util.concurrent` first.** An `ExecutorService` for running tasks, `AtomicInteger` for lock-free counters, `ConcurrentHashMap` for shared maps - written and tested by experts. Your hand-rolled version will be slower and buggier.

**Thread pools via `ExecutorService`.** Instead of a thread per task (exhausting the machine at a million tasks), a **thread pool** keeps a fixed crew of reusable threads pulling tasks from a queue. You submit work; the pool decides which thread runs it.

```mermaid
flowchart LR
  S[submit tasks] --> Q[task queue]
  Q --> T1[pool thread 1]
  Q --> T2[pool thread 2]
  Q --> T3[pool thread 3]
  T1 --> R[results]
  T2 --> R
  T3 --> R
```

**`Callable` and `Future`.** A `Runnable` returns nothing; a `Callable<T>` returns a value. Since the result isn't ready immediately, `submit` hands you a `Future<T>`, an IOU you cash in later with `.get()` (which blocks until the answer is ready).

```java
import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(3);

        Future<Integer> future = pool.submit(() -> {
            Thread.sleep(100);          // pretend this is slow work
            return 6 * 7;
        });

        System.out.println("doing other work while it computes...");
        Integer answer = future.get();   // blocks here until the result is ready
        System.out.println("the answer: " + answer);

        pool.shutdown();                 // stop accepting tasks; let running ones finish
    }
}
```
```console
$ java Main.java
doing other work while it computes...
the answer: 42
```
*What just happened:* `Executors.newFixedThreadPool(3)` made a pool of three reusable threads. `pool.submit(callable)` queued the work and immediately returned a `Future` - main kept going and printed its message *before* the result existed. `future.get()` then blocked until the pool thread finished and handed back `42`. ⚠️ Always `shutdown()` a pool when done; its threads are non-daemon by default and will keep the JVM alive otherwise.

**`CompletableFuture` for composing async work.** `Future.get()` is blocking and clumsy when chaining steps ("fetch data, *then* transform it, *then* save it"). `CompletableFuture` describes that pipeline declaratively and runs it without blocking:

```java
import java.util.concurrent.CompletableFuture;

public class Main {
    public static void main(String[] args) {
        CompletableFuture<String> pipeline =
            CompletableFuture.supplyAsync(() -> "data")        // step 1, on a background thread
                .thenApply(s -> s.toUpperCase())               // step 2, when step 1 finishes
                .thenApply(s -> "[" + s + "]");                // step 3, when step 2 finishes

        System.out.println(pipeline.join());                   // wait for the whole chain
    }
}
```
```console
$ java Main.java
[DATA]
```
*What just happened:* `supplyAsync` ran the first step on a background thread and returned a `CompletableFuture` immediately. `thenApply` attached the next step to run *automatically* when the previous one completed - no manual `get()`, no blocking between stages; the runtime wired up the handoffs. Modern Java composes async work this way: stages connected by `thenApply`, `thenCompose`, `thenCombine`, each firing when its input is ready.

💡 **Lock-free counters with `AtomicInteger`.** Remember the broken counter? `AtomicInteger` solves it without any lock you write: `count.incrementAndGet()` is a single atomic operation, implemented with a CPU compare-and-swap instruction - simpler and faster than `synchronized` for a shared counter. Same spirit: `ConcurrentHashMap` for a map many threads hammer at once. Reach for these before raw locks.

## Virtual threads - the modern leap

📝 **Virtual threads (Project Loom, standard since JDK 21) are extremely lightweight threads managed by the JVM rather than the OS.** A traditional ("platform") thread maps one-to-one onto a heavy OS thread - realistically a few thousand before memory runs out. A virtual thread is a cheap Java object; the JVM parks it off its underlying OS thread whenever it blocks (on I/O, a sleep, a lock) and reuses that OS thread elsewhere. You can run **millions** of them.

```java
import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        try (ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 1_000_000; i++) {
                pool.submit(() -> {
                    Thread.sleep(1000);   // a blocking call - fine here
                    return null;
                });
            }
        } // try-with-resources waits for all tasks to finish
        System.out.println("ran a million blocking tasks");
    }
}
```
```console
$ java Main.java
ran a million blocking tasks
```
*What just happened:* `newVirtualThreadPerTaskExecutor()` gave every one of a million tasks its *own* virtual thread - something that would instantly exhaust memory with platform threads. Each task blocked on `Thread.sleep`, but a blocked virtual thread costs almost nothing: the JVM unmounted it from its OS thread, freeing that thread to serve others.

**Why this matters for servers.** The old way to scale to many connections was reactive/async code - non-blocking callbacks, fast but genuinely hard to read and debug. Virtual threads let you write plain, straight-line **blocking** code ("read the request, query the DB, write the response") that scales to enormous concurrency anyway, because blocking is now cheap. Simple code, huge throughput.

⚠️ **Virtual threads do not repeal the laws of concurrency.** They're cheaper threads, not safer ones. Every race condition, every visibility bug, every **deadlock** - two threads each holding a lock the other needs, both stuck forever - is just as possible with a million virtual threads as with two platform threads. The new tools change the *cost* of concurrency, never the *correctness rules*. Shared mutable state still needs `synchronized`, atomics, or `java.util.concurrent` - that hard part never goes away, it just gets better tools.

## Recap

1. **A thread is an independent path of execution.** Wrap work in a `Runnable`/lambda and call **`start()`** (not `run()` - `run()` executes on the current thread and spawns nothing).
2. **A race condition** is a bug whose result depends on thread timing. `counter++` is read-add-write, not atomic, so concurrent increments lose updates - the heart of why concurrency is hard.
3. **`synchronized`** gives mutual exclusion (one thread at a time) *and*, via the **Java Memory Model's happens-before** rules, visibility. **`volatile`** gives visibility only - good for a flag, useless for read-then-write.
4. **Prefer `java.util.concurrent`.** An **`ExecutorService`** thread pool instead of raw threads, `Callable`/`Future` for results, **`CompletableFuture`** for async pipelines, and `AtomicInteger`/`ConcurrentHashMap` instead of hand-rolled locks.
5. **Virtual threads** (JDK 21+) are JVM-managed, near-free threads that let simple blocking code scale to millions - but ⚠️ they don't make races or **deadlocks** go away. Better tools, same rules.

You can now make programs do many things at once *and* keep their shared data intact - the skill that separates code that works on your laptop from code that survives production. Next: under the hood of the machine running all this - how the JVM manages memory, garbage collection, and just-in-time compilation.

## Quick check

Test yourself on the ideas that make concurrency safe rather than just fast:

```quiz
[
  {
    "q": "What's the difference between calling `thread.start()` and `thread.run()`?",
    "choices": [
      "`start()` spawns a new thread and runs the work there; `run()` is a plain method call that runs the work on the current thread - no new thread at all",
      "They're identical; `start()` is just the newer name for `run()`",
      "`run()` spawns the thread and `start()` waits for it to finish",
      "`start()` runs the work twice for safety"
    ],
    "answer": 0,
    "explain": "Only `start()` asks the OS for a new thread. `run()` is an ordinary method call that executes the Runnable on whoever called it - a common silent bug where 'concurrent' code mysteriously runs single-file."
  },
  {
    "q": "Two threads each run `counter++` 100,000 times on a shared int, and the total comes out less than 200,000. Why?",
    "choices": [
      "`counter++` is read-add-write - three steps - so two threads can read the same value and one increment gets lost; it's a race condition",
      "Java caps integer increments at 100,000 per variable",
      "The second thread silently fails to start",
      "Integer overflow wrapped the value around to a smaller number"
    ],
    "answer": 0,
    "explain": "`counter++` isn't atomic. Two threads can both read 5, both compute 6, both write 6 - two increments collapse into one. Repeated across the run, those lost updates produce a total below 200,000, different each run. Fix it with `synchronized` or `AtomicInteger`."
  },
  {
    "q": "Do virtual threads (JDK 21+) eliminate race conditions and deadlocks?",
    "choices": [
      "No - they make threads far cheaper, so blocking code scales to millions, but every concurrency hazard (races, visibility bugs, deadlocks) still applies and shared state still needs synchronization",
      "Yes - virtual threads are immune to race conditions by design",
      "Yes - the JVM automatically synchronizes all shared state for virtual threads",
      "Only deadlocks are eliminated; race conditions remain"
    ],
    "answer": 0,
    "explain": "Virtual threads change the cost of concurrency, not its correctness rules. A million virtual threads racing on a shared counter corrupt it exactly like two platform threads would. You still need synchronized, atomics, or java.util.concurrent."
  }
]
```


---

# The JVM: Memory, GC & JIT - What Runs Your Bytecode

You've written Java for fourteen phases and never told the machine where to put an object, never freed memory by hand, never thought about whether your code was "warm." That's the **JVM**, the Java Virtual Machine, sitting between your code and the hardware, quietly doing three jobs: running your compiled bytecode, managing all your memory, and rewriting hot code into native machine code while the program runs.

You can ship Java for years without opening this hood. But the moment you ask "why did my memory climb and never come back down?" or "why is the first request slow and the thousandth fast?", you're asking JVM questions. The big idea: **Java trades a little control you don't have for a lot of work you don't have to do** - and it pays off as long as you understand where it can still bite.

## The JVM, recapped and deepened

Back in the early phases, `javac` turned your `.java` files into `.class` files full of **bytecode** - a compact, portable instruction set no real CPU understands. Your bytecode doesn't run on an Intel or Apple-silicon chip; it runs on the JVM, a program pretending to be a CPU. "Write once, run anywhere" is exactly this: ship the same bytecode, and whatever JVM is installed translates it for the local hardware.

📝 **JVM (Java Virtual Machine)** - a runtime program that executes Java bytecode. It's *not* a sandbox you opt into; it's the thing your program runs *inside*, owning your memory, deciding where objects live, reclaiming them when you're done, and speeding up the code that runs often.

This is a **managed language**. In C, *you* manage memory - `malloc` and `free` - and getting it wrong means crashes and security holes. In Java, the JVM manages it: create objects with `new`, stop referring to them, and the JVM cleans up. The mental model for this phase: **you describe what you want; the JVM decides how and where.**

```java
public class Hello {
    public static void main(String[] args) {
        String name = "world";
        System.out.println("Hello, " + name);
    }
}
```
```console
$ javac Hello.java      # source -> bytecode (Hello.class)
$ java Hello            # the JVM loads Hello.class and runs its bytecode
Hello, world
```
*What just happened:* `javac` compiled your source into portable bytecode; `java` launched a JVM that loaded and executed it. The JVM allocated the `String`, managed the stack frame for `main`, and reclaimed everything on exit - none of which you wrote a line for. The rest of this phase zooms into each silent step.

## Stack vs heap - where your values live

The JVM splits memory into two regions with very different rules. Getting this distinction straight is the foundation for understanding garbage collection, recursion limits, and most memory bugs.

📝 **Stack** - a per-thread region that grows and shrinks with method calls. Each call pushes a **frame** holding that method's local variables (primitives like `int x = 5`, and *references* to objects). When the method returns, its frame pops and that memory vanishes instantly, for free. **Heap** - a single region shared by all threads, where every `new`-created object lives. The heap is *not* freed on return; reclaiming it is the garbage collector's job.

The crucial relationship: `Point p = new Point(3, 4)` does two things in two places. The `Point` object - its `x` and `y` fields - is built on the **heap**. The variable `p` is a local on the **stack**, and doesn't hold the object; it holds a **reference** (an arrow, an address) pointing at it. Primitives differ: `int n = 42` stores `42` directly in the stack frame, no heap, no arrow.

```mermaid
flowchart LR
  subgraph Stack["Stack (per thread)"]
    n["int n = 42"]
    p["Point p ───"]
  end
  subgraph Heap["Heap (shared)"]
    obj["Point { x=3, y=4 }"]
  end
  p --> obj
```

This explains a lot: two variables can point at the *same* heap object (why changing it through one shows through the other - aliasing); a method can return a reference and the object survives, since it lives on the heap, not the returning frame; and primitives copied between methods are genuinely independent, because the *value* travels, not an arrow.

```java
public class Where {
    static class Point { int x, y; Point(int x, int y) { this.x = x; this.y = y; } }

    public static void main(String[] args) {
        int n = 42;                         // value 42 lives in main's stack frame
        Point p = new Point(3, 4);          // the Point lives on the heap; p is a reference
        Point q = p;                        // q is a SECOND reference to the SAME object
        q.x = 99;                           // mutate through q...
        System.out.println(p.x);            // ...and p sees it, because they share one object
    }
}
```
```console
$ java Where
99
```
*What just happened:* `n` sat entirely in `main`'s stack frame as a raw value. The `Point` was allocated on the heap; both `p` and `q` are stack references holding the *same arrow* to it. Writing `q.x = 99` changed the one shared heap object, so `p.x` also read `99`. If `Point` had been copied by value (like `n`), `p` would still read `3` - the difference between "value lives here" and "arrow points there" is the whole game.

## Garbage collection

Objects pile up on the heap. In C you'd eventually `free()` each one; forget and you leak, double-free and you crash. Java removes that whole category of bug by reclaiming heap memory automatically, via the **garbage collector**.

📝 **Garbage collector (GC)** - finds heap objects your program can no longer reach and frees them, so you never call `free()` yourself. It works by **reachability**: starting from **GC roots** - local variables on every thread's stack, static fields, and a few others - it traces every reference it can follow. Reachable objects are *live* and kept; unreachable ones are garbage and get reclaimed.

The key shift in thinking: **you don't manage lifetimes - reachability *is* the lifetime.** An object lives exactly as long as something can reach it through a chain of references from a root. Drop the last reference, and it becomes eligible for collection. You never decide *when*, only *whether anything still points at it*.

```java
public class Reach {
    public static void main(String[] args) {
        byte[] data = new byte[10_000_000];   // ~10 MB on the heap, reachable via `data`
        System.out.println("allocated; reachable");
        data = null;                          // drop the only reference
        // The 10 MB is now UNREACHABLE - eligible for GC. We never free it ourselves.
        System.out.println("dropped; now eligible for collection");
    }
}
```
```console
$ java Reach
allocated; reachable
dropped; now eligible for collection
```
*What just happened:* The 10 MB array was reachable through the local `data`, a GC root. Setting `data = null` made it unreachable garbage. The next collection traces from the roots, never finds the array, and sweeps its memory back into the pool. We freed exactly nothing by hand.

**The generational hypothesis.** Tracing the *entire* heap on every collection would be slow. Real GCs exploit the empirical **generational hypothesis**: *most objects die young.* The temporary `StringBuilder` in a loop, the request object living one HTTP call - the overwhelming majority of allocations become garbage almost immediately, while a few (caches, long-lived services) stick around.

So the heap is split into generations:

```mermaid
flowchart LR
  subgraph Young["Young generation"]
    Eden["Eden (new objects)"]
    Surv["Survivor space"]
  end
  Old["Old generation (long-lived)"]
  Eden -->|survives a few collections| Surv
  Surv -->|keeps surviving| Old
```

New objects are born in the **young generation** (Eden). A **minor GC** collects just the young gen - frequent and fast, since it scans a small region where most objects are already dead. Survivors of several minor GCs get *promoted* to the **old generation**, collected less often by a bigger, slower **major GC** (or full GC). Collecting the young gen constantly and the old gen rarely does the least work for the most garbage reclaimed.

⚠️ Both kinds of collection can trigger a **stop-the-world (STW) pause** - a moment the JVM freezes all application threads so the collector can work on a stable heap snapshot. Modern collectors shrink these pauses dramatically, but they never vanish entirely.

**The collectors you'll meet.** Java ships several GC implementations, each a different point on the pause-time-vs-throughput trade-off:

- **G1 (Garbage-First)** - the default since Java 9. Splits the heap into regions, collecting incrementally to keep pauses predictable. A solid all-rounder.
- **ZGC** and **Shenandoah** - low-latency collectors doing almost all their work concurrently, targeting sub-millisecond pauses even on huge (multi-hundred-GB) heaps. Reach for these when a pause would be felt (trading systems, interactive services).
- **Parallel GC** - older, throughput-focused: longer pauses but maximum raw work done. Fine for batch jobs where total time matters more than smoothness.

Step through the mark-and-sweep cycle yourself - roots, reachable objects, the sweep - at your own pace:

```playground-gc
```

💡 **The insight.** GC is automatic, but not free - every object you allocate is work the collector must eventually do. Code churning through millions of short-lived objects creates **allocation pressure**: frequent minor GCs, eventually measurable pauses. The lever isn't usually a GC setting; it's allocating less garbage in the first place. You'll learn to *see* this in [Phase 17: Performance & Memory](17-performance-and-ecosystem.md); for now, hold the idea that "free" cleanup still has a price tag.

## JIT compilation

Your bytecode isn't native machine code, so *something* must translate it every time it runs. If the JVM translated every instruction on every execution, Java would be painfully slow. It isn't - a long-running Java service can match or beat C in throughput. The trick is the **JIT compiler**.

📝 **JIT (Just-In-Time) compiler** - compiles bytecode into native machine code *while the program runs*, focusing on methods that run often. The JVM starts by **interpreting** bytecode (reading and executing it instruction by instruction - quick to start, slow to run), watches which methods are "hot," and hands those to the JIT to compile into fast native code.

This is why Java has a **warm-up** period. The first time a method runs, it's interpreted; once the JVM notices it called thousands of times (or a loop spinning many iterations), it promotes the method to native code, running at full speed from then on. Your program literally gets faster the longer it runs, until it reaches a steady state.

Java's JIT works in **tiers** (tiered compilation):

- **C1 (client compiler)** - compiles quickly with light optimization. Gets hot code to "fast enough" sooner.
- **C2 (server compiler)** - compiles slowly with aggressive optimization (inlining, loop unrolling, dead-code elimination). For the *hottest* code, the JVM recompiles C1 output with C2 to squeeze out maximum speed.

The JVM also does **profile-guided** optimizations an ahead-of-time compiler can't: it watches the *actual* behavior of your running program - which branch is usually taken, which type shows up at a call site - and optimizes for that. If the assumption later proves wrong, it **deoptimizes** (falls back to the interpreter and recompiles). It optimizes for the real workload, not a compile-time guess.

```java
public class Warmup {
    static long work(long n) {
        long sum = 0;
        for (long i = 0; i < n; i++) sum += i % 7;   // hot loop: JIT will compile this
        return sum;
    }

    public static void main(String[] args) {
        for (int run = 0; run < 5; run++) {
            long start = System.nanoTime();
            work(50_000_000L);
            long ms = (System.nanoTime() - start) / 1_000_000;
            System.out.println("run " + run + ": " + ms + " ms");
        }
    }
}
```
```console
$ java Warmup
run 0: 41 ms        # interpreted / C1 - cold
run 1: 22 ms
run 2: 9 ms
run 3: 8 ms         # C2 has kicked in - warm, steady state
run 4: 8 ms
```
*What just happened:* The identical `work(50_000_000L)` call got dramatically faster across runs without changing a line. Run 0 ran cold - interpreted, then lightly compiled; by later runs the JIT had identified the hot loop, compiled it with C2's aggressive optimizations, and reached steady-state speed. The numbers vary by machine, but the *shape* - slow first, fast once warm - is universal.

⚠️ **This is why naive microbenchmarks lie.** Time a method once and report "Java did X in Y milliseconds," and you almost certainly measured interpretation and warm-up, not real performance - or a JIT that optimized away your unused result entirely. Accurate benchmarking requires *warming up* (running the code to steady state) before timing, plus tricks to stop the JIT from deleting ignored results. Don't hand-roll this; use the **JMH** harness, met in [Phase 16: Testing, Build & Profiling](16-testing-and-profiling.md).

## Class loading and memory errors

One more JVM job runs quietly under everything: getting your `.class` files into memory in the first place.

📝 **Class loader** - finds, loads, and links `.class` files **on demand**. A class isn't loaded when the JVM starts; it's loaded the first time your program needs it (the first time you reference the type). The bytecode is verified for safety, the class is initialized (static fields set, static blocks run), and it's ready to use. This lazy loading is why a huge application starts without reading every class up front.

Most of the time class loading is invisible. Where the JVM's memory model becomes *very* visible is when something goes wrong - two classic failures worth telling apart, since they come from the two memory regions you now understand.

**`OutOfMemoryError` - the heap filled up.** The collector tried to make room and couldn't: live objects already occupy the whole heap. Sometimes you genuinely need more memory; far more often it's a **leak** - objects you *think* are garbage are still reachable, so the GC can't touch them. Automatic GC frees the *unreachable*, not the *unused* - hold a reference, and the JVM keeps the object forever.

```java
import java.util.*;

public class Leak {
    static final List<byte[]> CACHE = new ArrayList<>();   // static = a GC root, lives forever

    public static void main(String[] args) {
        while (true) {
            CACHE.add(new byte[1_000_000]);                // never removed -> grows without bound
        }
    }
}
```
```console
$ java -Xmx64m Leak
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
```
*What just happened:* The `static` list `CACHE` is reachable from a GC root for the entire program, and so is every array added to it. Nothing is ever removed, so the GC correctly concludes it's all live - it can't reclaim a single byte. The heap (capped at 64 MB by `-Xmx64m`) fills and the JVM throws `OutOfMemoryError`. The GC works *perfectly*; the bug is ours, holding references we no longer need.

⚠️ **GC'd does not mean leak-proof.** Classic real-world leaks share this shape: a **static collection** (or singleton cache) that only ever grows, listeners or callbacks registered but never unregistered, and **unclosed resources** (streams, connections) whose buffers linger. The fix is never a GC flag - it's bounding lifetimes: evict from caches, deregister listeners, close resources (try-with-resources from [Phase 7: Errors & I/O](07-errors-and-io.md) - the pattern carries over).

**`StackOverflowError` - a stack frame too deep.** The *other* region failing. Each method call pushes a frame; recursion that never bottoms out pushes frames until the thread's stack space is exhausted.

```java
public class Deep {
    static int recurse(int n) {
        return recurse(n + 1);   // no base case -> frames pile up forever
    }
    public static void main(String[] args) {
        recurse(0);
    }
}
```
```console
$ java Deep
Exception in thread "main" java.lang.StackOverflowError
	at Deep.recurse(Deep.java:3)
	at Deep.recurse(Deep.java:3)
	...
```
*What just happened:* `recurse` calls itself with no base case, so every call pushes another frame and none ever pop. The stack is a bounded region; once full, the JVM throws `StackOverflowError`. The distinction: `OutOfMemoryError` is the **heap** (too many live objects), `StackOverflowError` is the **stack** (too many nested calls). Different region, different cause, different fix.

**The size dials.** Two flags set the heap's bounds: `-Xms` is the *initial* heap size, `-Xmx` the *maximum*. Setting `-Xmx512m` caps the heap at 512 MB; the program errors rather than consuming unbounded memory. In containers especially, set `-Xmx` deliberately so the JVM doesn't assume it owns the whole machine. Most apps never need more than these two, set once at launch.

## Recap

1. The **JVM** runs your portable bytecode, manages all your memory, and recompiles hot code to native speed - what makes Java a **managed** language: you describe what you want, the JVM decides how and where.
2. Memory splits into the **stack** (per-thread frames; local primitives and references; freed instantly on return) and the **heap** (shared; every `new` object; reclaimed only by the GC). A variable holds a *reference* (arrow) to a heap object, not the object itself.
3. The **garbage collector** frees **unreachable** objects by tracing from **GC roots** - reachability *is* the lifetime, so you never `free()`. The **generational hypothesis** (most objects die young) drives the young/old split: frequent fast **minor GCs**, rarer slow **major GCs**, with brief **stop-the-world** pauses. **G1** is the default; **ZGC**/**Shenandoah** target sub-millisecond pauses.
4. The **JIT compiler** interprets bytecode first, then compiles hot methods to native code (**C1** fast, **C2** aggressive) - why Java **warms up** and gets faster over time. ⚠️ Microbenchmarks must warm up or they lie - use JMH.
5. **Class loaders** load `.class` files **on demand**. ⚠️ `OutOfMemoryError` means the **heap** is full (often a leak via still-reachable objects - static collections, unclosed resources); `StackOverflowError` means the **stack** is too deep (runaway recursion). `-Xms`/`-Xmx` set the heap's bounds.

You now know what happens beneath `new`, `javac`, and every method call - the machine that runs your bytecode, places your objects, cleans up after you, and quietly makes your code faster the longer it runs. Next: making all of this measurable - testing, building, and profiling, watching allocations, GC pauses, and JIT warm-up with real tools instead of reasoning in the abstract.

## Quick check

Test yourself on the three ideas that matter most - where objects live, what GC actually frees, and why Java warms up:

```quiz
[
  {
    "q": "When you write `Point p = new Point(3, 4);`, where does the `Point` object live and what does `p` hold?",
    "choices": [
      "The Point object lives on the heap; `p` is a stack reference (an arrow) pointing at it",
      "Both the Point object and `p` live entirely on the stack",
      "The Point object lives on the stack; `p` is a heap reference pointing at it",
      "Both live on the heap, and the stack is only used for primitives like int"
    ],
    "answer": 0,
    "explain": "Objects created with `new` are built on the shared heap. The local variable `p` lives in the stack frame and holds a reference - an arrow - to that heap object, not the object itself. That's why two variables can point at the same object and a returned reference keeps its object alive."
  },
  {
    "q": "A `static List` keeps growing and you eventually get an `OutOfMemoryError`. Why can't the garbage collector reclaim those objects?",
    "choices": [
      "They are still reachable from a GC root (the static field), so the GC correctly treats them as live",
      "The GC only runs once at startup and never again during the program",
      "Static objects are immune to garbage collection by language rule",
      "The GC ran out of its own memory and gave up collecting"
    ],
    "answer": 0,
    "explain": "The GC frees what's unreachable, not what's unused. A static field is a GC root, so every object reachable through that ever-growing list stays live. The GC is working perfectly - the bug is holding references you no longer need. The fix is bounding the lifetime, not a GC setting."
  },
  {
    "q": "Why does the same method often run much faster on its thousandth call than its first?",
    "choices": [
      "The JIT compiler detected the method was hot and compiled it from bytecode to optimized native code",
      "The garbage collector deletes slow code paths after they run a few times",
      "Java caches the method's return value and skips running it again",
      "The class loader unloads and reloads the method in a faster form each call"
    ],
    "answer": 0,
    "explain": "The JVM interprets bytecode at first (slow), watches which methods run often, and hands the hot ones to the JIT, which compiles them to native code (C1 then C2). That warm-up is why steady-state Java is fast - and why naive one-shot microbenchmarks measure warm-up, not real performance."
  }
]
```


---

# Testing, Build & Profiling - Proving It Works, Finding the Slow Part

Back in [Phase 8](08-packages-and-tooling.md) you met the build tool that compiles, packages, and resolves dependencies - and a promise that "we cover testing in Phase 16." This is that phase. You can write Java that compiles and runs; what you can't yet do is *prove* it works, *measure* how fast it is, or *find* the slow part when it isn't.

The mental model for this phase: a real Java codebase doesn't run on hope. It runs on a test suite that fails loudly when behavior breaks, on measurements instead of guesses about speed, and on a profiler that points at the actual hot spot rather than the one you suspected. The JVM ships a mature toolchain for all of it - JUnit for correctness, JMH for accurate benchmarks, Java Flight Recorder for profiling. "Is it correct?" and "is it fast?" stop being arguments in code review and become commands you run.

## JUnit 5 - the basics and the AAA shape

📝 **Unit test** - a small automated test that exercises one method (or class) with a known input and asserts the expected result. It's "unit" because it tests one thing in isolation, not the whole app wired together. It runs in milliseconds, needs no database or network, and fails loudly the moment the behavior it pins down changes. **JUnit 5** is the de facto framework that runs these and reports pass/fail.

Without tests, "it works" means "it worked the one time I ran it by hand." A unit test turns that into a permanent, repeatable claim: run the suite, and every method that ever had a test still behaves. The payoff isn't catching today's bug - it's catching the one you'll introduce six months from now when you refactor and forget an edge case.

A JUnit test is a method annotated `@Test` that calls **assertions** - `assertEquals`, `assertTrue`, `assertThrows` - which fail the test if reality disagrees. The shape that keeps tests readable is **AAA: Arrange, Act, Assert** - set up the inputs, call the thing once, check the result. Say we're testing `Clamp`, which pins a number into a `[min, max]` range:

```java
// Clamp.java
package com.example.mathx;

public class Clamp {
    public static int clamp(int n, int min, int max) {
        if (n < min) return min;
        if (n > max) return max;
        return n;
    }
}
```

```java
// ClampTest.java
package com.example.mathx;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class ClampTest {

    @Test
    void clampsValueBelowMinimum() {
        // Arrange
        int n = -3, min = 0, max = 10;
        // Act
        int result = Clamp.clamp(n, min, max);
        // Assert
        assertEquals(0, result);
    }

    @Test
    void throwsIsNotOurJobButWeCanCheckOne() {
        // assertThrows verifies the call raises the exception you expect
        assertThrows(ArithmeticException.class, () -> {
            int x = 1 / 0;
        });
    }
}
```

```console
$ mvn test
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.example.mathx.ClampTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] -------------------------------------------------------
[INFO] BUILD SUCCESS
```

*What just happened:* Each `@Test` method is one independent test JUnit discovers and runs. The first follows AAA exactly - arrange the inputs, act by calling `clamp` once, assert the result equals `0`. `assertEquals(expected, actual)` fails the test (printing both values) if they differ; `assertThrows` is the assertion for the *unhappy* path, running the lambda and passing only if the expected exception is thrown. We ran it with `mvn test` (Gradle's `./gradlew test` is the same idea). ⚠️ Note the `assertEquals(expected, actual)` order: swap them and failure messages read backwards, maddening at 2am.

💡 **Key point.** The AAA shape isn't ceremony - it's what makes a test readable as a *specification*. Anyone can scan "given `-3, 0, 10`, expect `0`" and understand the contract without reading `clamp`'s body. A test that mixes setup, calls, and checks into a tangle is a test nobody trusts.

## Parameterized & nested tests - one method, many cases

You just wrote one test for "below min," but `clamp` has at least four interesting cases: inside the range, below min, above max, exactly on a boundary. Copy-pasting `clampsValueBelowMinimum` four times - changing only the numbers - is exactly the duplication that rots. JUnit's answer is the **parameterized test**: one test method, fed many sets of inputs.

📝 **Parameterized test** - a single `@ParameterizedTest` method JUnit runs once per data row you supply. Inputs come from a source annotation; the method body is the shared assertion logic - Java's version of table-driven testing.

```java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertEquals;

class ClampParamTest {

    // @ValueSource: one parameter per run - every value here is already in range,
    // so clamping should return it unchanged.
    @ParameterizedTest
    @ValueSource(ints = {0, 5, 10})
    void valuesInsideRangePassThrough(int n) {
        assertEquals(n, Clamp.clamp(n, 0, 10));
    }

    // @CsvSource: multiple parameters per run - "input, expected" rows.
    @ParameterizedTest(name = "clamp({0}) -> {1}")
    @CsvSource({
        "-3, 0",    // below min
        "99, 10",   // above max
        "7,  7"     // inside
    })
    void clampsToBounds(int input, int expected) {
        assertEquals(expected, Clamp.clamp(input, 0, 10));
    }
}
```

```console
$ mvn test
[INFO] Running com.example.mathx.ClampParamTest
clamp(-3) -> 0  ✔
clamp(99) -> 10 ✔
clamp(7)  -> 7  ✔
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0
```

*What just happened:* `@ValueSource` fed three single `int` values into one method, so JUnit ran it three times. `@CsvSource` fed three comma-separated rows, each unpacked into `input` and `expected` - the same shared assertion checked all three. Six logical tests, two methods, zero copy-paste. `name = "clamp({0}) -> {1}"` labels each run with the parameter values, so a failure tells you *which row* broke.

💡 The behavior under test is now a *visible list* of cases. A reviewer can scan the rows and ask "where's the case for `min > max`?" - nearly impossible when each scenario hides in its own method.

Two more annotations round this out. **`@DisplayName`** attaches a human sentence to a test or class, so reports read like English instead of method names. **`@Nested`** groups related tests in an inner class - useful for organizing by scenario:

```java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

@DisplayName("Clamp")
class ClampNestedTest {

    @Nested
    @DisplayName("when value is out of range")
    class OutOfRange {
        @Test
        @DisplayName("pins values below min up to min")
        void belowMin() {
            assertEquals(0, Clamp.clamp(-5, 0, 10));
        }

        @Test
        @DisplayName("pins values above max down to max")
        void aboveMax() {
            assertEquals(10, Clamp.clamp(50, 0, 10));
        }
    }
}
```

*What just happened:* `@Nested` turns `OutOfRange` into a sub-group, and the `@DisplayName`s make the test report read as a nested outline - "Clamp › when value is out of range › pins values below min up to min." That structure is documentation that can't go stale, since it fails if the behavior it describes breaks.

## Mocking - testing a unit in true isolation

Real methods rarely live alone. An `OrderService` calls a `PaymentGateway`; a `UserService` hits a database. To test the *service* without firing real payments or needing a live database, replace the dependency with a **mock** - a fake stand-in you control. **Mockito** is the standard library for this.

📝 **Mock** - a controllable stand-in for a real dependency. Tell it what to return (`when(...).thenReturn(...)`), exercise the code under test, then verify it called the dependency correctly (`verify(...)`) - testing one unit while its collaborators behave however the scenario needs.

```java
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

class OrderServiceTest {

    @Test
    void chargesTheGatewayAndReturnsSuccess() {
        // Arrange: a fake gateway that "succeeds" without touching a real network
        PaymentGateway gateway = mock(PaymentGateway.class);
        when(gateway.charge(100)).thenReturn(true);

        OrderService service = new OrderService(gateway);

        // Act
        boolean ok = service.placeOrder(100);

        // Assert: the result, and that we actually called charge() once with 100
        assertTrue(ok);
        verify(gateway).charge(100);
    }
}
```

*What just happened:* `mock(PaymentGateway.class)` created a fake gateway whose methods do nothing by default. `when(gateway.charge(100)).thenReturn(true)` programmed it: "if asked to charge 100, say success." We injected that fake into `OrderService`, ran `placeOrder`, and checked the returned result plus (via `verify`) that `charge(100)` was called exactly once. No real payment, no network, fully deterministic.

⚠️ **Don't over-mock.** Mocks are for *boundaries you can't or shouldn't hit in a test* - payment gateways, email senders, the network. Mock your own plain value objects and internal helpers, and your tests stop verifying behavior and start verifying that your code calls itself in a particular order - they break on every refactor and prove almost nothing. Mock the edges; use the real thing everywhere else.

## Benchmarking with JMH - why naive timing lies

Correctness is one question; *speed* is another - and here Java sets a trap that catches nearly everyone. The instinct is to wrap your code in `System.nanoTime()` calls and a loop. On the JVM, that number is **a lie**, tying straight back to the JIT compiler from [Phase 15](15-the-jvm-memory-and-gc.md).

⚠️ **You cannot time Java reliably with `System.nanoTime` in a loop.** Two forces sabotage you. **JIT warmup**: the first thousands of iterations run interpreted or half-optimized, and the JIT only compiles your hot loop to native code *after* it's deemed hot - time the whole loop and you average cold and hot together. **Dead-code elimination**: if you compute a result and never use it, the JIT can delete the computation entirely, so you end up benchmarking an empty loop and reporting an impossibly fast "result."

📝 **Why naive microbenchmarks lie.** A hand-rolled `nanoTime` loop measures a moving target (the JIT optimizing mid-run) and an unstable one (the optimizer may delete code whose result you discard). The fix isn't more careful manual timing - it's a framework built to defeat exactly these effects.

That framework is **JMH** (Java Microbenchmark Harness), the official OpenJDK tool. Annotate a method with `@Benchmark`; JMH runs dedicated *warmup* iterations to let the JIT settle, runs your code in a fresh JVM *fork* (so one benchmark can't pollute another's optimization state), and consumes your return values through a `Blackhole` so the optimizer can't delete them. You get an accurate number with error bars.

```java
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5)   // let the JIT warm up before measuring
@Measurement(iterations = 5)
@Fork(1)
@State(Scope.Benchmark)   // required so JMH can hold the instance field `parts`
public class StringBuildBenchmark {

    private final String[] parts = {"a", "b", "c", "d", "e", "f", "g", "h"};

    @Benchmark
    public String concatWithPlus() {
        String s = "";
        for (String p : parts) {
            s += p;   // each += builds a brand-new String
        }
        return s;     // returned, so the JIT can't delete the work
    }

    @Benchmark
    public String concatWithBuilder() {
        StringBuilder sb = new StringBuilder();
        for (String p : parts) {
            sb.append(p);
        }
        return sb.toString();
    }
}
```

```console
$ java -jar target/benchmarks.jar StringBuildBenchmark
# JMH version: 1.37
# Warmup: 5 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
...
Benchmark                              Mode  Cnt    Score    Error  Units
StringBuildBenchmark.concatWithPlus    avgt    5  118.402 ±  4.211  ns/op
StringBuildBenchmark.concatWithBuilder avgt    5   42.097 ±  1.640  ns/op
```

*What just happened:* JMH ran each `@Benchmark` through 5 discarded warmup iterations (letting the JIT compile the hot path) and 5 measured iterations, in a forked JVM. Output reads in **ns/op** with a `± Error` margin, so you know it's stable, not noise. The numbers: `+=` in a loop (~118 ns) is nearly three times slower than `StringBuilder` (~42 ns), because each `+=` allocates a brand-new `String` (strings are immutable) while the builder mutates one buffer. Returning the result keeps the JIT from deleting the loop - the `+=`-versus-builder lesson you might have *guessed*, now with a defensible number.

💡 **Key point.** Reach for JMH only when genuinely comparing two implementations or chasing a measured hot spot - not to micro-time everything. But when you *do* benchmark, use JMH: a hand-rolled timing loop on the JVM doesn't just give an imprecise answer, it gives a confidently *wrong* one.

## Profiling & coverage - find the real hot spot, map the untested

A benchmark tells you *that* one method is slow when you already suspected it - it won't tell you *where* a whole running application spends its time. For that you need a **profiler**.

📝 **Profiler** - a tool that samples your running program to record where it actually spends CPU time and allocates memory, then shows the answer ranked worst to best. It replaces "I think this function is the bottleneck" with "here is the measured bottleneck."

The JVM has an unusually strong profiling story, all production-grade:

- **Java Flight Recorder (JFR)** - built *into the JVM itself*, low enough overhead to leave on in production. Start a recording (`java -XX:StartFlightRecording=...`) to capture CPU, allocation, lock, and GC events into a `.jfr` file, then open it in **JDK Mission Control (JMC)** for a visual breakdown of hot methods and allocation sources.
- **VisualVM** - a free, friendly graphical profiler for development: attach to a running JVM and watch CPU, heap, and threads live.
- **async-profiler** - a low-overhead sampling profiler popular for *flame graphs*, where each bar's width shows how much total time a call path consumed - great for spotting a hot path at a glance.

```console
$ java -XX:StartFlightRecording=duration=60s,filename=app.jfr -jar myapp.jar
[1.234s][info][jfr] Started recording 1. Will dump to app.jfr after 60 s.
...
$ jfr print --events jdk.ExecutionSample app.jfr | head
# (or open app.jfr in JDK Mission Control for the visual view)
```

*What just happened:* `-XX:StartFlightRecording` told the JVM to record 60 seconds of execution into `app.jfr` while the app ran normally - no code changes, negligible overhead. Opening that file in Mission Control ranks methods by how often the profiler caught them running, so the real CPU hog rises to the top. `jfr print` is the command-line peek at the same data.

💡 **Measure, don't guess.** Every engineer has a confident hunch about the bottleneck, and it's wrong often enough to waste real days: you optimize the method you *suspected*, ship it, and the app is exactly as slow, because the true cost lived somewhere you never looked. Profile first, then optimize what the profile points at - the evidence-gathering step [Phase 17](17-performance-and-ecosystem.md) builds on.

The other side of "did I test enough?" is **code coverage** - how much of your code the test suite actually executed. **JaCoCo** is the standard tool; build tools wire it in and produce a color-coded report:

```console
$ mvn test    # with the jacoco-maven-plugin configured
[INFO] --- jacoco:report ---
[INFO] Analyzed bundle 'mathx' with 3 classes
[INFO] Coverage: 82% of lines, 75% of branches
# open target/site/jacoco/index.html - green = covered, red = never ran
```

*What just happened:* JaCoCo instrumented the code during the test run and tracked which lines and branches executed. The HTML report colors your source: green lines ran during tests, red lines never did. The red is the valuable part - it shows the branches your tests forgot, like the `min > max` case nobody wrote.

⚠️ **Coverage is not correctness.** The trap everyone walks into. Coverage tells you a line *executed* - nothing about whether you *checked the result*, or whether the input that breaks it was ever tried. A test that calls `clamp` once and asserts nothing can light up the whole method green. Treat coverage as a *map of the untested* - chase the red - never as a score to maximize. High coverage with weak assertions is more dangerous than plain medium coverage, because it *feels* safe.

## Recap

1. A **unit test** is a `@Test` method that asserts one method's behavior for a known input. Follow the **Arrange-Act-Assert** shape so the test reads as a spec, run it with `mvn test` / `./gradlew test`, and mind the `assertEquals(expected, actual)` argument order.
2. **`@ParameterizedTest`** with `@ValueSource` / `@CsvSource` runs one method over many input rows - Java's table-driven testing - while `@DisplayName` and `@Nested` make the report read like documentation.
3. **Mockito** replaces a real dependency with a controllable mock (`mock` / `when` / `verify`) so you can test a unit in isolation; ⚠️ mock the boundaries, not your own internals.
4. ⚠️ You **cannot** time Java with a `nanoTime` loop - JIT warmup and dead-code elimination make the number a confident lie. **JMH** (`@Benchmark`) handles warmup, forking, and result-sinking, and reports accurate **ns/op** with error bars.
5. **Profilers** - **JFR** + JDK Mission Control, VisualVM, async-profiler - find the real CPU/allocation hot spot. 💡 Measure, don't guess: profile first, then optimize what the profile points at.
6. **JaCoCo** maps which lines and branches your tests ran - chase the red - but ⚠️ coverage is not correctness: an executed line is not a checked one.

You can now prove your Java is correct, measure how fast it is accurately, and find the slow part with evidence instead of instinct. Next: widening the lens to performance patterns and the broader ecosystem that turns those measurements into action.

## Quick check

Test yourself on the ideas that separate "I ran it once" from "I proved it works and measured it":

```quiz
[
  {
    "q": "Why can't you reliably benchmark Java code with a `System.nanoTime()` loop, and what does JMH do about it?",
    "choices": [
      "JIT warmup and dead-code elimination distort the result; JMH adds warmup iterations, JVM forking, and a Blackhole so the numbers are accurate",
      "nanoTime() isn't precise enough; JMH uses a higher-resolution clock",
      "Loops are always optimized away in Java; JMH disables the JIT entirely",
      "nanoTime() returns wall-clock time; JMH measures CPU time instead"
    ],
    "answer": 0,
    "explain": "A naive loop averages cold interpreted runs with hot JIT-compiled ones, and the optimizer can delete computations whose results you never use. JMH runs dedicated warmup iterations so the JIT settles, forks a fresh JVM, and consumes return values through a Blackhole so the work can't be eliminated."
  },
  {
    "q": "Your JaCoCo report shows 100% line coverage. What does that actually guarantee?",
    "choices": [
      "Every line executed at least once during the tests - nothing about whether results were asserted",
      "The code has no bugs",
      "Every possible input was tested",
      "Every assertion in the suite passed"
    ],
    "answer": 0,
    "explain": "Coverage only measures which lines ran. A test that calls a method and asserts nothing still marks those lines green. Treat coverage as a map of the untested - chase the red - because high coverage with weak assertions feels safe but proves almost nothing."
  },
  {
    "q": "What is a `@ParameterizedTest` and why prefer it over copy-pasting a `@Test` method four times?",
    "choices": [
      "One test method JUnit runs once per input row, so the cases become a visible, reviewable list instead of duplicated code",
      "A test that automatically generates random inputs to find crashes",
      "A test that runs in parallel across multiple CPU cores",
      "A faster kind of test that skips the JUnit lifecycle"
    ],
    "answer": 0,
    "explain": "A parameterized test feeds many input rows (via @ValueSource or @CsvSource) through one shared assertion. Adding a scenario is a new row, not a new method, and a reviewer can scan the rows to spot a missing case - far harder when each scenario hides in its own copy-pasted method."
  }
]
```


---

# Performance & the Ecosystem

Here's the thing nobody warns you about when you start chasing speed: most of your code is already fast enough, and most of your guesses about *where* it's slow will be wrong. Performance work isn't clever tricks - it's a discipline. Measure, find the one place that actually matters, fix that, and stop. The discipline is what separates a real speedup from an afternoon of busywork that moved nothing.

This phase caps the deep half of the guide, leaning on two things you already have: the memory model from [Phase 15](15-the-jvm-memory-and-gc.md) (heap, object lifetimes, the garbage collector) and the benchmarking/profiling tools from [Phase 16](16-testing-and-profiling.md) (JFR, JMH). We'll put those to work in the order that pays off: measure, fix the algorithm, cut allocations - then step back and look at where Java takes you next.

## Measure first, always

Your intuition about performance is a liar - not because you're bad at this, but because the JVM, JIT compiler, CPU caches, and garbage collector interact in ways no human predicts reliably. The method you're *sure* is the bottleneck is often a rounding error, while the real cost hides in a string concatenation you never thought twice about. Worse, the JIT may have already optimized away the very thing you were about to "fix." The only way to know is to look.

⚠️ **The number-one rule of optimization: never optimize on a hunch.** Speed something up without a measurement proving it was slow *and* a measurement proving your change helped, and you're gambling - the usual prize is uglier code that runs exactly as fast (or slower, having defeated an optimization the JIT was doing for free). Profile first. Always.

The workflow, from Phase 16, used in anger:

1. Write a benchmark (JMH) or capture a recording (JFR) that exercises the real, representative work.
2. Look at where time - and allocation - actually goes.
3. Fix the single biggest cost.
4. Re-run the benchmark to *prove* the fix helped. Repeat from step 2.

```console
$ java -jar benchmarks.jar -prof gc
Benchmark                       Mode  Cnt    Score    Error  Units
findDuplicates.slow             avgt    5   41.382 ±  1.04   ms/op
findDuplicates.slow:·gc.alloc   avgt    5    0.512 ±  0.02   MB/op
```
*What just happened:* JMH ran the benchmark many times (after warming up the JIT - critical on the JVM, where the first runs are interpreted and misleadingly slow), then reported the average time per operation and, via `-prof gc`, how much memory each operation allocated. That allocation column is a hint we'll return to. (These numbers are from one run on one machine; yours will differ - the *shape*, one operation dominating, is what matters.)

💡 **Key insight.** In almost every program, a tiny fraction of the code accounts for the overwhelming majority of runtime. Your job isn't to make *everything* fast - it's to find that hot 3% and leave the other 97% alone, readable and untouched. Profiling finds the 3%; polishing the rest is wasted effort that only adds risk.

## Algorithmic cost dominates

Before fiddling with a single allocation, ask the bigger question: *is the approach itself right?* The largest performance wins almost never come from micro-tweaks - they come from replacing a fundamentally expensive strategy with a cheaper one, turning an O(n²) nested scan into an O(n) pass with a hash-based lookup. No low-level cleverness rescues a quadratic algorithm; it just makes the cliff arrive slightly later.

If "O(n²)" and "O(n)" feel fuzzy, the dedicated primer [Big-O Without the Math Panic](/guides/big-o-without-the-math-panic) walks through exactly what they mean and why they decide who wins as your data grows.

The classic case: find which names in a list appear in a big set of known names. The naive version scans the list for each lookup - `List.contains` walks the whole list every time, so checking `n` names against a list of `m` is O(n·m):

```java
// O(n·m): contains() scans the entire list on every single check.
static List<String> matchSlow(List<String> queries, List<String> known) {
    List<String> hits = new ArrayList<>();
    for (String q : queries) {
        if (known.contains(q)) {   // linear scan of `known` every time
            hits.add(q);
        }
    }
    return hits;
}
```

The `HashSet` version does the same job, but each lookup is roughly constant-time instead of a full scan:

```java
// O(n): a HashSet makes each lookup ~constant-time.
static List<String> matchFast(List<String> queries, Set<String> known) {
    List<String> hits = new ArrayList<>();
    for (String q : queries) {
        if (known.contains(q)) {   // hash lookup - no scan
            hits.add(q);
        }
    }
    return hits;
}
```

*What just happened:* both methods look almost identical at the call site, but the cost curves aren't. `List.contains` walks the list element by element, so checking many queries against a large list grows with the *product* of the two sizes. `HashSet.contains` jumps straight to the slot via the element's hash, so each lookup is roughly constant regardless of how many names are stored - invisible on 10 items, instant-versus-coffee-break on 100,000. (This is why a stray `List.contains` inside a loop is one of the most common accidental performance bugs in Java.)

Benchmark them side by side and the gap is brutal:

```console
$ java -jar benchmarks.jar Match
Benchmark            Mode  Cnt      Score     Error  Units
matchSlow            avgt    5   38_400.0 ± 900.0   us/op
matchFast            avgt    5       21.4 ±   0.6   us/op
```
*What just happened:* against 10,000 queries over a 10,000-element collection, the set-based version is over a thousand times faster (`us/op` = microseconds per operation, lower is better), and that multiplier *grows* with input: double both sizes and the list version quadruples while the set version merely doubles. Algorithm choice dwarfs everything else. (Exact numbers vary by machine and JVM; the order-of-magnitude gap doesn't.)

Play with how each growth curve behaves as `n` climbs - it makes the O(n²)-vs-O(n) gap concrete in a way numbers on a page can't:

```playground-bigo
```

## Allocations & GC pressure - the usual Java bottleneck

Once your algorithm is sound, the most common remaining drag in Java is *heap allocation*. Recall from [Phase 15](15-the-jvm-memory-and-gc.md): every `new` puts an object on the heap, and every heap object is something the garbage collector must later track and reclaim. Java's GC sweeps up short-lived objects cheaply - but "cheaply" isn't "free." Churn out millions of throwaway objects in a hot loop and the GC runs more often, stealing CPU and adding pauses. In Java, "make it faster" very often means "make it allocate less."

📝 **GC pressure** - the rate at which your code creates garbage the collector must clean up. Fewer short-lived objects means less frequent collection: lower CPU overhead *and* steadier, more predictable latency under load. See allocation per operation with JMH's `-prof gc` (the `gc.alloc.rate.norm` line: bytes allocated per op).

The single most famous offender is building a string with `+=` in a loop. Strings are immutable, so each `+=` doesn't grow the string - it allocates a *brand-new* string and copies the entire contents so far. Build a string from `n` pieces this way and you allocate and copy O(n²) characters total:

```java
// Wasteful: each += allocates a new String and copies everything so far.
static String joinSlow(List<String> parts) {
    String out = "";
    for (String p : parts) {
        out += p;          // new String allocated + full copy, every iteration
    }
    return out;
}

// Lean: one growing buffer, no per-iteration copies.
static String joinFast(List<String> parts) {
    StringBuilder sb = new StringBuilder();
    for (String p : parts) {
        sb.append(p);      // appends into the existing buffer
    }
    return sb.toString();  // one final String at the end
}
```

*What just happened:* `joinSlow` creates a fresh `String` every iteration and copies all characters accumulated so far, so the work (and garbage) grows with the *square* of the number of parts. `joinFast` writes into a single `StringBuilder`, a mutable buffer growing in chunks that only materializes one `String` at the end - same output, a fraction of the allocations. (The compiler can rewrite a *simple* `a + b + c` into a single optimized concatenation for you, but not across a loop - reach for `StringBuilder` yourself there.)

```console
$ java -jar benchmarks.jar Join -prof gc
Benchmark                         Mode  Cnt      Score   Units
joinSlow                          avgt    5   2_140.3   us/op
joinSlow:·gc.alloc.rate.norm      avgt    5  10_240_512 B/op
joinFast                          avgt    5      14.7   us/op
joinFast:·gc.alloc.rate.norm      avgt    5      24_576 B/op
```
*What just happened:* joining 1,000 strings, the `StringBuilder` version is roughly a hundred times faster and allocates a few hundred times *less* memory (`B/op` is bytes per operation). The `+=` version churned out megabytes of intermediate strings the GC had to reclaim; the buffer version allocated essentially one buffer. Less garbage, less GC work, a large free speedup. (Numbers vary by machine and JVM version; the direction is rock-solid.)

The same "stop making needless garbage" principle shows up in three other everyday spots:

- **Presize your collections.** `new ArrayList<>()` starts with a small backing array and reallocates a bigger one (copying everything) each time it fills up. Knowing roughly how many elements you'll add, `new ArrayList<>(expectedSize)` (or `HashMap` with an initial capacity) reserves the room up front - one allocation instead of a series of resize-and-copies.
- **Avoid needless autoboxing in hot loops.** Recall from [Phase 15](15-the-jvm-memory-and-gc.md) that `int` is a primitive but `Integer` is a heap object. Summing into a `List<Integer>` or a `Long` accumulator boxes every value - millions of tiny objects in a hot loop. Keep the loop counter and accumulator as primitives (`int`, `long`); let boxing happen only where you genuinely need an object.
- **Reach for primitive arrays when it truly matters.** An `int[]` stores raw ints packed together; an `ArrayList<Integer>` stores *pointers* to boxed `Integer` objects scattered on the heap - far more memory, far worse cache behavior. For large, hot numeric data, a primitive array can be a big win. (Measure first - for most code, `ArrayList<Integer>` is perfectly fine and more convenient.)

## Choosing collections & APIs wisely

A surprising amount of Java performance is picking the right tool from the standard library. The defaults are good; the wrong default in a hot path is not.

- **`ArrayList` vs `LinkedList`.** Reach for `ArrayList` almost always - a contiguous array: fast indexed access, cache-friendly iteration, low overhead. `LinkedList` wins only for frequent insertion/removal *at the ends* via a deque interface, and even then `ArrayDeque` usually beats it (its per-element node objects are pure allocation and cache-miss overhead). If you typed `new LinkedList`, you probably wanted `ArrayList`.
- **`HashMap` vs `TreeMap`.** `HashMap` gives ~O(1) lookup and is the default. Use `TreeMap` only when you genuinely need keys in sorted order (range queries, ordered iteration) - it's O(log n) per operation and slower otherwise. Don't pay for ordering you don't use.
- **Streams vs loops in hot paths.** ⚠️ Streams are readable and almost always fast enough - use them freely for clarity. But a stream pipeline can allocate more than a plain loop (lambdas, intermediate objects, boxing for `Stream<Integer>` vs `IntStream`). In a genuinely hot inner loop profiling flagged, a plain `for` loop (or `IntStream`) sometimes wins. The rule isn't "avoid streams" - it's "measure before rewriting readable stream code into a loop for speed."

The theme: readable defaults everywhere, deliberate exceptions only where a measurement told you to make one.

## Knowing when to stop - and the ecosystem ahead

Optimization has a point of diminishing - then *negative* - returns. Every clever rewrite makes code harder to read, harder to change, easier to break. That cost is real, paid by every future reader, including you in six months. The goal is never "as fast as physically possible" - it's "fast enough for the actual requirement, and no more twisted than it has to be."

💡 **The closing rule of the deep half.** Readable code that's fast enough beats clever code that's unmaintainable, every time. Define "fast enough" up front (a target like "p99 under 50ms" or "processes the batch in under a second"), optimize the *one* measured hot path, re-measure after every change, and revert anything that didn't move the number. Then - the hardest part - stop.

You've reached the top of the mountain. You understand Java from `javac` turning source into bytecode, through the JVM loading and JIT-compiling it, down to how the garbage collector manages memory - and how to make all of it faster on purpose, with evidence instead of guesses.

That foundation is what the **Java ecosystem** is built on. Here's where it takes you next, the landscape Phase 18 maps in full:

- **Spring & Spring Boot** - the dominant backend Java framework. Build web services, APIs, or microservices in Java for a living, and you'll almost certainly build them on Spring Boot, standing on everything you learned about objects, generics, exceptions, and the JVM.
- **Jakarta EE** - the enterprise standard (formerly Java EE): servlets, persistence (JPA), dependency injection, and the specs much of the server-side world is built around.
- **Android** - Java (alongside Kotlin) powers a huge share of the world's mobile apps. Same language; the platform and lifecycle are what you'd learn next.
- **Build & observability tooling** - Maven and Gradle for builds, plus profiling/monitoring tools (JFR, Micrometer, and friends) that make this phase's measurement discipline a permanent part of how you ship.

You don't need to learn these now - just know they exist, and that you're ready for them.

## Recap

1. **Measure first, always.** Never optimize on a hunch. Use JMH and JFR to find the real hot spot; most code is already fast enough, so hunt the 3% that isn't - the JIT may have already optimized what you were about to touch.
2. **Algorithmic cost dominates.** The biggest wins come from a better approach (a `HashMap`/`HashSet` lookup instead of an O(n·m) `List.contains` scan), not micro-tweaks - you can't optimize your way out of the wrong complexity class.
3. **Allocations & GC pressure are the usual Java bottleneck.** Fewer short-lived objects means less GC work, faster and steadier code. Use `StringBuilder` over `+=` in loops, presize collections, avoid needless autoboxing in hot loops, and prefer primitive arrays when it truly matters.
4. **Choose collections and APIs wisely.** `ArrayList` over `LinkedList`, `HashMap` unless you need sorted keys, streams freely for readability - but measure before rewriting a hot stream pipeline into a loop.
5. **Know when to stop.** Define "fast enough" up front, optimize the one measured hot path, re-measure after every change, revert what didn't help. Readable-and-fast-enough beats clever-and-unmaintainable.
6. **You're ready for the ecosystem.** You understand Java from `javac` to the GC - now Spring, Jakarta EE, Android, and build/observability tooling are the natural next steps.

That's the deep half done. The final phase maps the whole landscape and points you at where to go from here.

## Quick check

Test yourself on the discipline that makes performance work actually pay off:

```quiz
[
  {
    "q": "Before changing any code to make a Java program faster, what should you do first?",
    "choices": [
      "Profile with JMH or JFR to find where time and allocation actually go",
      "Replace every ArrayList with a LinkedList",
      "Rewrite the slowest-looking method from memory",
      "Add StringBuilder everywhere a String appears"
    ],
    "answer": 0,
    "explain": "Intuition about bottlenecks is unreliable, and the JIT may already be optimizing what you'd change. Measure first with JMH/JFR so you fix the real hot spot - the small fraction of code that actually dominates runtime - instead of guessing."
  },
  {
    "q": "You need to check 10,000 names against a collection of 10,000 known names. Which single change usually delivers the biggest speedup on large inputs?",
    "choices": [
      "Store the known names in a HashSet so each lookup is ~constant-time instead of a List.contains scan",
      "Switch the result list from ArrayList to LinkedList",
      "Remove all comments from the hot loop",
      "Rename the variables so the JIT optimizes better"
    ],
    "answer": 0,
    "explain": "Algorithmic complexity dominates. List.contains scans linearly, so it's O(n·m); a HashSet lookup is roughly constant-time, turning the work into O(n). That margin grows with input size - no micro-optimization rescues the wrong complexity class."
  },
  {
    "q": "Why does building a string with += inside a loop create so much GC pressure?",
    "choices": [
      "Strings are immutable, so each += allocates a brand-new String and copies all the characters so far",
      "+= secretly calls the garbage collector on every iteration",
      "String concatenation is not allowed inside loops and throws at runtime",
      "Each += converts the string to a byte array and back"
    ],
    "answer": 0,
    "explain": "A String can't be modified in place, so += produces a fresh String and copies the accumulated contents every iteration - O(n²) characters of garbage. A StringBuilder writes into one growing buffer and materializes a single String at the end."
  }
]
```


---

# Where to Go Next - Putting Java to Work

Notice what you've done. You didn't only learn a language - you learned the *platform* it runs on: objects, generics, collections, exceptions, threads, the standard library, and the JVM underneath (bytecode, garbage collection, the JIT). That's not a beginner's slice of Java - that's the real thing, language and runtime both.

This last phase isn't more syntax - everything from here is *application*, pointing what you know at a real target. The Java world is huge, and it's tempting to feel you must learn all of it at once. You don't. Here's the clear map of where Java genuinely shines, and - most importantly - what to build so it sticks.

## The branches from here

```mermaid
flowchart TD
  You[You: solid Java + the JVM] --> SB[Spring Boot backend]
  You --> AND[Android apps]
  You --> BIG[Big data & distributed]
  You --> EE[Jakarta EE]
  SB --> Job[Most Java jobs]
```

*What this shows:* four directions lead out from where you stand, and one - **Spring Boot** - is where most Java careers actually live. You don't have to pick forever, but pick *one to go deep on next*: depth beats breadth when learning, and going wide too early leaves you with four shallow puddles instead of one well.

## Backend with Spring Boot - the dominant Java job

Chase only one of these, and make it this one. **Spring Boot** is, for most people, the highest-leverage next step - the framework behind a huge share of Java backend jobs, where "Java developer" in a posting very often means "Spring Boot developer."

What it gives you: **dependency injection** (the framework wires your objects together so you stop doing it by hand), **REST APIs** (write a method, annotate it, it answers HTTP requests), and **JPA** (describe your data as plain Java classes and it talks to the database for you). It builds directly on the objects, interfaces, and generics you already know - the annotations are new, the foundation isn't.

The leap from here is short *because* you understand the Java underneath. People who jump straight into Spring Boot without the language often spend months unsure where the framework's magic ends and their own code begins - you won't have that fog.

## Android - Java still works, concepts transfer

**Android** is the other place a lot of Java has historically lived. The straight update: **Kotlin** is now Google's primary recommended language for Android, and most new code is written in it. But Java still works for Android, and Kotlin runs on the *same JVM* you just learned - its objects, collections, null-handling, and concurrency are concepts you already have, with friendlier syntax on top.

The Java you learned isn't wasted here even if you write Kotlin. If building things people tap on their phones excites you, this path is open, and your foundation carries over almost completely.

## Big data & distributed systems - much of this world is the JVM

A corner of the industry that surprises people: a large slice of **big data and distributed systems** runs on the JVM. **Apache Kafka** (event-streaming), **Apache Spark** (large-scale data processing), and **Elasticsearch** (search at scale) are all JVM software, as is much of the infrastructure around them.

That means your Java - and your understanding of threads, memory, and the JVM from Phases 14 and 17 - is a real entry ticket to data engineering and distributed-systems work. Drawn to systems moving enormous amounts of data? You're already in the right ecosystem.

## Jakarta EE - the enterprise standard

**Jakarta EE** (formerly Java EE) is the long-standing set of enterprise standards - servlets, persistence, messaging, and more - powering many large, established corporate systems. It's heavier and more ceremonial than Spring Boot, less likely to be where you start today. But it's worth *knowing the name*: join a big enterprise with a mature Java codebase, and there's a fair chance some of it lives here - a path to recognize and step into later, not your first stop.

💡 **Why Java keeps winning.** Not hype - the opposite. Java's superpower is **stability plus ecosystem plus the JVM**: decades of battle-tested libraries, world-class tooling (IDEs, profilers, build systems), and a runtime that stays backward-compatible for years. That's "boring technology," meant as a compliment - big systems want code that keeps working, that they can hire for, that won't break on the next upgrade. Exactly what Java offers, why it's still everywhere a decade after people predicted its decline.

## What to actually build

Reading guides got you here; *building* turns knowledge into skill. The trick is something small enough to finish but real enough to teach you the messy parts. A few no-nonsense suggestions:

- **A REST API with Spring Boot, backed by a database.** A handful of endpoints - create, read, list, delete - storing data through JPA. The single most career-relevant thing you can build, making dependency injection and persistence concrete.
- **A command-line tool.** Take a chore you do by hand - renaming files, summarizing a log, checking a list of URLs - and make it a Java program, exercising classes, collections, file I/O, and exceptions at once, no framework in the way.
- **A multi-threaded downloader.** Fetch several URLs at once using the concurrency tools from [Phase 14](14-concurrency-and-threads.md) - watching real work happen in parallel makes threads stop being abstract.

Whichever you pick, the real instruction is: **finish one.** A finished rough project teaches far more than three polished half-projects abandoned at 80%. Pick the one that excites you, build it end to end, and ship it even if it's small.

## A last word, and what to read

Two resources worth bookmarking. The **official Java tutorials and documentation** on the Oracle/OpenJDK sites are thorough and trustworthy - maintained by the people who build Java. Once you're comfortable, **"Effective Java"** by Joshua Bloch is the canonical book on Java idioms - where good Java developers learn to write *idiomatic* Java rather than merely working Java.

If you ever want to step back and think about *why* languages make the choices they do - why Java reaches for a VM, a garbage collector, and static types while another language reaches for something else entirely - that's [Languages, Explained Like a Human](/guides/languages-explained-like-a-human). A good companion now that you've lived inside one language, and its runtime, end to end.

You came in not knowing Java. You're leaving able to read it, write it with real objects and generics, handle exceptions and threads without flinching, and reason about the JVM beneath your code. That's a genuine, hireable skill - and the ecosystem it opens is one of the largest in software. Go build the small thing. You're ready.

## Recap

1. **You learned the language *and* the JVM** - everything from here is applying that foundation, not starting over.
2. **Spring Boot is the highest-leverage next step for most** - dependency injection, REST APIs, and JPA, and it's where most Java jobs are. Go deep here first.
3. **The other branches:** Android (Kotlin is primary now, but JVM concepts transfer), big data and distributed systems (Kafka, Spark, Elasticsearch - much of it is JVM), and Jakarta EE for established enterprise codebases.
4. **Java keeps winning** on stability, ecosystem, and the JVM - "boring tech that keeps working," exactly what big systems want.
5. **Build one real thing and finish it** - a Spring Boot REST API, a CLI tool, or a multi-threaded downloader - leaning on the standard library and what you already know.
6. **Next reading:** the official Java docs and *Effective Java* (Bloch) for idioms; [Languages, Explained Like a Human](/guides/languages-explained-like-a-human) for the bigger picture.

## Quick check

Test yourself on the one decision that matters most here - where to point your Java next:

```quiz
[
  {
    "q": "For most people, which path is the highest-leverage next step after learning Java?",
    "choices": [
      "Spring Boot - dependency injection, REST APIs, and JPA, and where most Java jobs are",
      "Jakarta EE, because it's the oldest and therefore the most important",
      "Learning a second language immediately before building anything in Java",
      "Android, because Java is still the primary recommended Android language"
    ],
    "answer": 0,
    "explain": "Spring Boot is behind a huge share of Java backend jobs, and the leap is short because it builds directly on the objects, interfaces, and generics you already know. Android's primary language is now Kotlin, and Jakarta EE is heavier and less common as a starting point."
  },
  {
    "q": "Why is the Java you learned still valuable even if you do Android development with Kotlin?",
    "choices": [
      "Kotlin runs on the same JVM, so your objects, collections, and concurrency concepts transfer directly",
      "Android refuses to run any Kotlin code unless Java is also present",
      "Kotlin is just Java with a different file extension and identical syntax",
      "Google requires every Android app to include at least one Java class"
    ],
    "answer": 0,
    "explain": "Kotlin compiles to and runs on the JVM you just learned, so the underlying model - objects, collections, null-handling, concurrency - is the same. The syntax differs, but your foundation carries over almost completely."
  },
  {
    "q": "What's the most important rule when choosing a project to build next?",
    "choices": [
      "Pick one small-but-real project and finish it end to end",
      "Start three ambitious projects so you cover more ground at once",
      "Only build something if you can use all four branches in it",
      "Avoid databases and concurrency until you've read every Java book"
    ],
    "answer": 0,
    "explain": "A finished rough project teaches far more than several polished half-projects abandoned at 80%. Pick one that excites you - a Spring Boot API, a CLI tool, or a multi-threaded downloader - and ship it, even if it's small."
  }
]
```
