Records, Sealed Classes & Modern Java Features

Course 2 · Ch 7
Records, Sealed Classes & Modern Java Features
The direct bridge back to Kotlin's own data classes and sealed classes

This is the chapter that closes the loop this whole track opened: Java exists specifically to retroactively supply the fluency Kotlin's own course always assumed, and Kotlin's most distinctive features — data classes, sealed classes — turn out to have direct Java counterparts, added relatively recently.

Records — Data Carriers Without the Boilerplate

public record Point(int x, int y) {} Point p1 = new Point(3, 4); p1.x(); // 3 — accessor named x(), not getX() p1.equals(new Point(3, 4)); // true — generated automatically p1.toString(); // "Point[x=3, y=4]" — generated automatically

One line generates a constructor, accessor methods (x()/y()), and correct equals()/hashCode()/toString() implementations — a direct, real payoff of java2-2's own equals()/hashCode() contract, generated for free instead of hand-written and risked getting wrong. A record's fields are always final — genuinely immutable by design, not merely by convention.

Records Are Genuinely Comparable to Kotlin's data class

Kotlin's own data class auto-generates the identical set of methods — equals(), hashCode(), toString(), plus a copy() Java's record doesn't directly provide. This is a real, direct parallel — the same underlying idea (a small, deliberately boilerplate-free data carrier), arriving in each language on its own schedule.

Sealed Classes/Interfaces

public sealed interface Shape permits Circle, Square, Triangle {} public record Circle(double radius) implements Shape {} public record Square(double side) implements Shape {} public record Triangle(double base, double height) implements Shape {}

sealed combined with permits declares a genuinely closed, exhaustively-known set of subtypes — nothing outside the permits list can ever implement Shape. This is a direct parallel both to Kotlin's own sealed class material and to rust1-6's data-carrying enums, which achieve the same closed-set guarantee through a different mechanism entirely.

Exhaustive switch Pattern Matching Over Sealed Types

double area = switch (shape) { case Circle c -> Math.PI * c.radius() * c.radius(); case Square s -> s.side() * s.side(); case Triangle t -> 0.5 * t.base() * t.height(); // no default needed — the compiler already knows these three cases are the ONLY possible ones };

This is java1-3's light pattern-matching touch, finally paid off in full: because Shape is sealed, the compiler knows the complete, closed set of subtypes and can verify every one is handled — a missing case is a compile error, not a runtime surprise. Combining a closed type hierarchy with exhaustive matching is a genuine safety win neither piece delivers alone.

The Bridge Back to Kotlin

Kotlin's own sealed class plus a when expression is checked exhaustively the exact same way — the compiler refuses to compile a when over a sealed type that's missing a branch. This Java feature exists specifically to retroactively supply the concept Kotlin's own course used without ever building it from first principles — the loop this entire Java track was written to close.

FeatureKotlinRust (rust1-6)Java
Boilerplate-free data carrierdata classderive macros (rust3-4)record
Closed set of subtypessealed classa data-carrying enumsealed interface/class + permits
Exhaustive matchingwhen (compiler-checked)match (compiler-checked)switch over a sealed type (compiler-checked)
Reach for a record whenever a class is just data
If a class's entire job is holding a fixed set of immutable values with no extra behavior, a record replaces pages of hand-written constructor/accessor/equals/hashCode/toString boilerplate with one line, with no risk of the hand-written version drifting out of sync.
A record can't extend another class
Every record implicitly extends java.lang.Record already, and java1-5's single-inheritance rule means that slot is taken — a record can still implement any number of interfaces, but it can never extends a regular class of its own.

Coding Challenges

Challenge 1

Write a record Person(String name, int age), create two instances with identical values, and print whether they're equal and what toString() produces — without writing any of those methods yourself.

📄 View solution
Challenge 2

Write a sealed interface Vehicle permitting exactly Car and Motorcycle (as records), then write a switch expression over a Vehicle that returns each type's wheel count, with no default branch, and explain why omitting a case would fail to compile.

📄 View solution
Challenge 3

Attempt to write a record that extends a regular class. Show the resulting compile error and explain why in terms of java1-5's single-inheritance rule and what a record already implicitly extends.

📄 View solution

Chapter 7 Quick Reference

  • record generates a constructor, accessors, equals()/hashCode()/toString() automatically — fields are always final
  • Genuinely comparable to Kotlin's own data class — same idea, different arrival schedule
  • sealed + permits declares a closed, exhaustively-known set of subtypes, paralleling Kotlin's sealed class and rust1-6's data-carrying enums
  • switch over a sealed type is compiler-verified exhaustive — a missing case is a compile error, no default needed
  • A record can't extend a regular class — it already implicitly extends java.lang.Record, and java1-5's single-inheritance rule applies
  • Next chapter: the capstone — a real Java project combining OOP, generics, collections, streams, and exception handling