Projects — Phase 04¶
You have read about generics, streams, Optional, and CompletableFuture. Reading does not transfer. These three projects make the ideas muscle memory: rewrite a real analyzer with streams and prove it’s not slower, build a small type-safe DSL that forces you to use bounded generics for real, and drill 30 imperative→stream conversions until the idiomatic form comes without thinking. Ship all three to a public repo — they double as portfolio pieces.
Project A · Log-File Analyzer, Streams Edition (with Benchmarks)¶
You wrote a log-file analyzer in Phase 01 as imperative Java (BufferedReader + for loops + HashMap). Now rewrite it end-to-end with Files.lines, Stream, and Collectors — and JMH-benchmark both versions so you know whether streams cost you anything on real data.
Scope¶
Take an access-log file (Apache/Nginx combined format is easiest — public sample datasets on Kaggle work; NASA HTTP log is a canonical choice at ~1.9M lines). Produce these outputs:
Requests per status-code bucket (2xx / 3xx / 4xx / 5xx).
Top 10 URLs by hit count.
Top 10 client IPs by bytes served.
Requests per hour-of-day histogram.
p50 / p95 / p99 response size across all successful requests.
Required techniques¶
Parse lines into a
record LogEntry(String ip, Instant ts, String method, String url, int status, long bytes)— records for value objects, per Phase 03.Use
Files.lines(path)inside a try-with-resources so the file handle closes.Use
Collectors.groupingBy,counting,summingLong,partitioningBy, andteeingat least once each.Compute percentiles with
mapToLong→IntSummaryStatisticsfor the mean, and a sortedlong[]for p95/p99.Return an immutable
AnalysisReportrecord aggregating all five metrics.
Benchmarking¶
Add a benchmark/ module with JMH (org.openjdk.jmh:jmh-core:1.37). Compare:
Imperative for-loop version
Sequential stream version
Parallel stream version (
.parallel())
Report throughput (ops/s) and allocation rate (JMH -prof gc). Write a BENCHMARKS.md with a small table and a one-paragraph honest verdict — most students discover that:
Sequential streams are within 5-15% of imperative, sometimes faster (compiler hoisting).
Parallel streams win only past ~500k lines on 8+ cores, and lose otherwise (the DZone/Langer result).
Allocation rate is higher for streams (boxed objects) but rarely matters.
Definition of done¶
Two implementations, one report record, one JMH module.
Repo README with the results table and honest verdict.
Unit tests confirming both implementations produce identical
AnalysisReportfor a fixed input.No
.parallel()in your code without a justifying comment referencing the JMH numbers.
Project B · Type-Safe Validation DSL¶
Build a small validation library using generics + functional interfaces. The API should read like English at the call site and refuse invalid types at compile time — not at test time.
Target API¶
Validator<User> userValidator = Validator.<User>of()
.field(User::name, notBlank().and(maxLength(80)))
.field(User::email, matches(EMAIL_REGEX))
.field(User::age, between(0, 150));
ValidationResult result = userValidator.validate(user);
if (result.hasErrors()) return badRequest(result.errors());
Required techniques¶
Validator<T>is a generic class holding a list of field-level rules.field(Function<T, F> getter, Rule<F> rule)uses bounded generics so you cannot pass aRule<Integer>for aStringfield — the compiler catches the mismatch.Rule<F>is a functional interfaceFunction<F, Optional<String>>— returns an error message or empty for OK.Built-in combinators:
and,or,negate; usePredicatecomposition.Built-in rules:
notNull,notBlank,maxLength(int),minLength(int),between(min, max),matches(Pattern),nonEmpty()for collections.ValidationResultis a record:record ValidationResult(Map<String, List<String>> errors) { boolean hasErrors() { ... } }.Optional stretch: a custom
Collector<Rule<F>, ?, Rule<F>>that composes rules viaand.
Constraints¶
No reflection. This is a generics exercise — everything must be type-safe at compile time.
No dependency on Jakarta Bean Validation, Hibernate Validator, or third-party libs.
Public methods must have JUnit 5 tests covering: happy path, single-field failure, multi-field failure, chained rules.
Why this matters¶
You are simultaneously exercising PECS (rule combinators consume ? super F and produce ? super F), bounded generics (getter type must match rule type), functional composition (and/or/negate), and record-based results. That’s four Phase 04 concepts in ~300 lines.
Alternative: if you already work on a query-heavy service, build a tiny type-safe query builder instead — same generics muscles, different domain. Something like:
Query<User> q = Query.from(User.class)
.where(User::age, gt(18))
.where(User::email, endsWith("@zoho.com"))
.orderBy(User::name)
.limit(50);
Pick whichever domain will show up in your day job first.
Project C · 30 Stream Refactoring Katas¶
Drill until the idiomatic form appears without thought. This is the highest ROI exercise in the phase — every code review you fail on streams is one of these 30 shapes.
Format¶
Set up a katas/ module. For each kata, you get:
An
Imperative.javafile with a small imperative method (loops, mutable maps, temp lists).A
Streams.javafile with the same method signature, empty body.A
KataNTest.javafile with property-based tests (jqwik) or exhaustive JUnit tests that must pass for both.
Your job: implement the streams version, run the tests, then compare against a reference solution in a hidden solutions/ folder (only unlock after you’ve committed your attempt).
The 30 katas¶
Group by increasing difficulty. The list is deliberately practical — every shape here comes from real Zoho / Spring / enterprise codebases.
Level 1 — Basic transforms (1-8)
Sum of integers in a list.
Uppercase all strings, filter out blanks.
Count elements matching a predicate.
Find first element matching a predicate (return
Optional).Map list of
Orderto list oforderId(long).Group
Orderby status, count per group.Partition numbers into even/odd.
Convert
List<Employee>toMap<Long, Employee>by id.
Level 2 — Collectors and grouping (9-16)
Group orders by customer, sum total per customer (BigDecimal).
Top 5 customers by lifetime spend.
Group products by category, get max-priced product per category.
Compute avg / min / max of a numeric field with
summarizingDouble.Join names into a CSV string (
Collectors.joining(", ", "[", "]")).Deduplicate a list preserving order (
LinkedHashSetvia collector).Collectors.teeing— compute mean and count in one pass.Group by two fields (nested groupingBy).
Level 3 — flatMap, Optional, and control flow (17-24)
Flatten
List<List<String>>intoList<String>.From
List<Order>where each order hasList<Item>, get all items flat.Given
List<Optional<User>>, getList<User>(Java 9+Optional::stream).Zip two lists into a list of pairs (hint:
IntStream.range).Compute a running sum (prefix sums) — force yourself to use
Stream.reduceor streams-plus-mutable-accumulator; note the mutable-accumulator smell.From
List<Employee>, find the department with the highest average salary.Return first N distinct elements matching a predicate.
Convert
Map<String, List<Order>>toMap<String, BigDecimal>(total per customer) without loops.
Level 4 — Traps and edge cases (25-30)
Group-then-reduce that mishandles empty groups — fix it so empty groups return
BigDecimal.ZERO, not throw.A
parallel()version of kata 9 — benchmark and decide whether to keep it.A pipeline that side-effects into a shared
Map— refactor toCollectors.toMap.Convert an imperative pagination loop (fetch pages until empty) to
Stream.iteratewith atakeWhile(Java 9+).A collector that produces an immutable
List— useCollectors.toUnmodifiableListor.stream().toList()and explain the difference.Rewrite a Comparator-heavy sort using
Comparator.comparing(...).thenComparing(...).reversed().
Rules¶
Each kata gets its own commit with the message
kata NN: <one-line summary>.Every kata must have a failing test written first, then a passing implementation.
If a kata is easier in a
for-loop, note that in the file — some are. Not every problem wants to be a stream.
Time budget¶
Roughly 1-2 hours per kata. Do 4-5 per week over the phase. By the end you should be able to write any of them in under 3 minutes without looking anything up.
Delivery Timeline¶
Week |
Focus |
Milestone |
|---|---|---|
1-2 |
Generics + Project B skeleton |
Type-safe DSL passes 3 tests |
3 |
Lambdas / method refs |
Katas 1-8 done |
4 |
Streams anatomy |
Project A imperative version → stream version, no benchmarks yet |
5 |
Collectors deep dive |
Katas 9-16 done, Project A benchmarks running |
6 |
Optional |
Katas 17-24 done, Project B validators complete with tests |
7 |
CompletableFuture |
Katas 25-30 done, Project A BENCHMARKS.md published |
8 |
Polish + write-up |
All three projects public on GitHub with READMEs |
What This Phase Gives You in studies¶
Post-phase you can look at any .stream()... line and tell me why it exists — not just what it does. You can defend or refuse parallel() with numbers. You can explain PECS at a whiteboard without hedging. And you can compose async values without blocking a pool. That’s a mid-to-senior signal in every Java study.
Ship the three projects. Post the log-analyzer benchmarks in a small blog post — hiring managers read those and they take about 90 minutes to write once the data is in hand.
Return to README.md · Previous: 05_concurrency_primer_functional.md