Phase 04 — Generics, Streams, and the Functional Edge of Modern Java

Why This Phase Exists

Every mid-level Java engineer thinks they know generics, streams, and Optional. Most know them well enough to compile, not well enough to reason. This is the phase where study partners separate seniors from mid-levels. You will meet bounded wildcards in a real signature, be asked why List<Object> is not List<String>, and be handed an imperative loop to convert to streams — with the study partner watching to see if you write parallel() for a 10-element list.

Phase 03 taught you shapes (records, sealed, patterns). This phase teaches you pipelines — how values flow through functions with type-safety intact. It also teaches you when the functional style hurts and imperative wins. Both matter.

Weekly Schedule (Months 5–6, roughly 8 weeks)

Week

Focus

Key file

1

Type erasure, bounded types, wildcards

01_generics_deep.md

2

PECS + generic methods + Class<T> token

01_generics_deep.md

3

Functional interfaces + lambda capture

02_functional_interfaces_lambdas.md

4

Method references + composition

02_functional_interfaces_lambdas.md

5

Stream anatomy + common mistakes

03_streams_the_honest_guide.md

6

Collectors + parallel reality

03_streams_the_honest_guide.md

7

Optional done right

04_optional_done_right.md

8

CompletableFuture + virtual threads bridge

05_concurrency_primer_functional.md

Interleave projects.md throughout — the log-analyzer rewrite starts in week 5, the DSL in week 6, the katas from day one.

Exit Criteria

You are done with Phase 04 when you can:

  • Read and write bounded wildcards (? extends T, ? super T) without hesitation, and explain PECS in one sentence

  • Explain type erasure and its three practical consequences (no new T[], no T.class, no reified type at runtime)

  • Write a stream pipeline that a reviewer says “reads like intent, not mechanics”

  • Refuse parallel() in a code review with a concrete reason

  • Use Optional only as a return type, and prefer map/filter/orElseThrow over isPresent/get

  • Compose a 3-stage CompletableFuture pipeline with proper exception handling

  • State one situation where virtual threads (Java 21) replace CompletableFuture chains entirely

  • Ship the three projects in projects.md

File Map

File

Topic

Weight

01_generics_deep.md

Type erasure, bounds, wildcards, PECS, generic methods

Heavy

02_functional_interfaces_lambdas.md

Function/Predicate/Consumer/Supplier, method refs, capture

Medium

03_streams_the_honest_guide.md

Pipeline anatomy, laziness, collectors, parallel() reality

Heavy

04_optional_done_right.md

Return-type-only rule, safe idioms, weaker than Result

Medium

05_concurrency_primer_functional.md

CompletableFuture, virtual threads bridge to Phase 05

Medium

projects.md

Log analyzer rewrite, type-safe DSL, 30 stream katas

Ship these

Books and References

  • Effective Java, 3rd ed (Bloch, 2018) — Items 26–33 (generics), 42–48 (lambdas and streams), 55 (Optional). Still the reference in 2026 despite the 8-year gap; the Reddit and Coderanch consensus is that no book has replaced it.

  • Java Concurrency in Practice (Goetz et al, 2006) — dated on APIs but the mental models under CompletableFuture and virtual threads still trace back here.

  • Modern Java in Action (Urma, Fusco, Mycroft — Manning, 2019) — the streams/functional deep dive Bloch does not fully cover.

  • Angelika Langer’s Java generics FAQ (free online) — the definitive reference on bounded wildcards, capture conversion, and every generics edge case. Bookmark it.

What Most People Get Wrong

  1. Optional as a field type. It is a return type. Serializers hate it. It adds 16 bytes per field. Bloch is explicit (Item 55).

  2. parallel() sprinkled on every stream. DZone’s classic benchmark (Angelika Langer) shows parallel streams can be 15× slower for some workloads. Nested parallelStream() inside another parallel stream shares the same ForkJoinPool.commonPool() — a known anti-pattern.

  3. Collectors.toList() in Java 16+ code. Since Java 16, stream.toList() gives you an unmodifiable list in one call. Use it unless you specifically need a modifiable ArrayList.

  4. Raw types “just to shut up the compiler.” List raw = new ArrayList() disables all generic checking on that variable — the warnings you suppressed were the compiler protecting you.

  5. Lambdas that need three-line comments. If a lambda body exceeds five lines, extract a private method with a name. The functional style is not a license for cleverness.

study Signals

study partners listening for seniority will ask:

  • “What does List<? extends Number> let you do that List<Number> does not?” → covariance

  • “When does List<String> refuse to be a List<Object>?” → invariance and safety

  • “You have a stream over 10 million records — parallel or sequential?” → “It depends; here is how I’d measure.”

  • “Return type: User or Optional<User> or @Nullable User?” → tradeoffs, team convention, serialization impact

  • “CompletableFuture vs virtual threads for this HTTP fanout?” → virtual threads if the code is naturally imperative; CF if you already have a functional pipeline

Answer each of these confidently by phase end.


Return to ../README.md · Previous phase: ../03_oop_design_patterns_modern_java/README.md · Next: 01_generics_deep.md