Checkpoint: Practical Java

This checkpoint ties together the practical-Java track — collectors, virtual threads, the HTTP client, equals/hashCode, immutability, the builder pattern, dependency injection, and serialization — into one combined build challenge.

Learn Checkpoint: Practical Java in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…

Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

A recap of eight lessons, a multi-step build challenge (an immutable record assembled by a builder, grouped with collectors, with a correct equals/hashCode), a written checkpoint quiz, and a 10-question timed quiz. If anything here feels shaky, revisit that lesson before moving to the final project.

💡 Analogy: Up to now you've been collecting individual tools — a saw here, a clamp there. A checkpoint is where you build something using several tools at once . The challenge below makes one small program that needs a record (the material), a builder (the jig that assembles it), equals/hashCode (so duplicates don't sneak in), and collectors (to measure and sort the finished pieces). That's how real code feels — patterns combine.

Complete the starter below: finish the record's validation, the nested Builder , and the four stream pipelines. Then compare against the full compiled solution. The expected output is shown in the comments.

A record built by a builder, deduplicated by its generated equals / hashCode , and grouped four ways with Collectors:

Real output (compiled and run with javac 21 ):

Notice how every concept appears: the record gives free equals/hashCode (so the duplicate Apple collapses, leaving 5 distinct), the builder assembles each product with defaults and validation, and collectors compute totals, counts, and the dearest item per category — all in deterministic TreeMap order.

Answer: the record auto-generates equals / hashCode from all components, so two Apples with the same name, category and price are equal and hash to the same bucket — the HashSet treats them as one.

Answer: sorted, deterministic key order. Without it you'd get a HashMap whose printed order is undefined.

Answer: it runs for every way the record is created (including via the builder's build() ), so an invalid Product can never exist.

Answer: submit each HTTP send to Executors.newVirtualThreadPerTaskExecutor() — virtual threads let thousands of blocking calls run concurrently without exhausting OS threads.

Answer: defensively copy it in the compact constructor with tags = List.copyOf(tags) , so neither the caller's list nor the accessor can mutate the record.

Answer: maxBy reduces each group to an Optional<Product> ; the finishing function opt.map(Product::name).orElse("none") turns it into just the name.

You combined records, builders, equals/hashCode and collectors into one working program, and recapped the whole practical track — collectors, virtual threads, the HTTP client, immutability, dependency injection, and serialization. These are the patterns you'll reach for constantly in real Java.

Next up: the Final Project — bring everything together into one substantial application.

Practice quiz

Which collector groups elements and totals a numeric field per group?

  • toMap(k, f)
  • partitioningBy(f)
  • groupingBy(k, summingInt(f))
  • joining(f)

Answer: groupingBy(k, summingInt(f)). groupingBy with a summingInt downstream produces a Map of key to the integer total per group.

Why do records pair so well with hash-based collections?

  • They auto-generate consistent equals and hashCode
  • They are static
  • They are faster to allocate
  • They skip validation

Answer: They auto-generate consistent equals and hashCode. A record's generated equals/hashCode satisfy the contract, so records work correctly as Set elements and Map keys.

Where should a Builder validate the object it constructs?

  • In each setter
  • In toString
  • In main
  • In build() (or the product's constructor)

Answer: In build() (or the product's constructor). build() is the single point where the whole object exists; for a record the compact constructor also validates.

What scales virtual threads for I/O-bound work?

  • They use more CPU
  • Blocked threads unmount and free their carrier
  • They never block
  • They avoid the heap

Answer: Blocked threads unmount and free their carrier. A blocked virtual thread is unmounted so its carrier serves others — millions of mostly-waiting tasks become feasible.

What must you check after HttpClient.send for a 404?

  • response.statusCode()
  • Catch an exception
  • response.error()
  • Nothing — it throws

Answer: response.statusCode(). HTTP error statuses are returned, not thrown; inspect statusCode() yourself.

Defensive copying in an immutable class prevents...

  • faster code
  • compilation
  • callers mutating your internal collection
  • serialization

Answer: callers mutating your internal collection. Copying mutable inputs/outputs (e.g. List.copyOf) stops outside code from changing your state.

Constructor injection improves testability because...

  • it removes interfaces
  • you can inject fakes instead of real collaborators
  • it runs faster
  • it avoids constructors

Answer: you can inject fakes instead of real collaborators. Depending on an injected interface lets tests pass a fake and assert on interactions with no real I/O.

Which field keyword excludes a field from serialization?

  • volatile
  • static
  • final
  • transient

Answer: transient. transient fields are skipped and return to defaults after deserialization.

What does Collectors.collectingAndThen do?

  • Sorts the stream
  • Collects then applies a finishing function
  • Counts elements
  • Splits the stream

Answer: Collects then applies a finishing function. It runs a collector and post-processes the result — e.g. mapping a maxBy Optional to a name, or making a list unmodifiable.

If a.equals(b) is true, the contract requires...

  • a == b
  • a.toString() == b.toString()
  • a.hashCode() == b.hashCode()
  • nothing

Answer: a.hashCode() == b.hashCode(). Equal objects must have equal hash codes, or hash collections break.