Learning goals
By the end of this chapter you should be able to:
- Declare records for immutable data carriers and add compact validation in compact constructors
- Use sealed classes/interfaces to restrict subtypes and model closed hierarchies
- Apply pattern matching for
instanceofandswitch(JDK 21) - Destructure record patterns in switch and if
Records — concise data classes
A record declares immutable data with generated equals, hashCode, toString, and accessors:
public record Point(int x, int y) {
public Point {
if (x < 0 || y < 0) throw new IllegalArgumentException("negative");
}
public double distanceFromOrigin() {
return Math.hypot(x, y);
}
}
- Components (
x,y) areprivate finalfields with accessor methodsx(),y()— not JavaBeangetX. - Canonical constructor can be compact (above) or explicit.
- Records are
final— cannot extend another class (may implement interfaces).
Point p = new Point(3, 4);
Point moved = new Point(p.x() + 1, p.y());
Use records for DTOs, value objects, AST nodes — not for JPA entities requiring mutable state or no-arg constructors.
Sealed types — closed hierarchies
Sealed restricts who may extend/implement:
public sealed interface Shape permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double w, double h) implements Shape {}
public final class Triangle implements Shape { /* ... */ }
Every permitted subtype must be final, sealed, or non-sealed. Enables exhaustive switch without default when combined with pattern matching.
Model domain sums: Result.Success | Result.Failure, payment methods, expression trees.
Pattern matching for instanceof
Before Java 16:
if (obj instanceof String) {
String s = (String) obj;
use(s);
}
Modern:
if (obj instanceof String s && !s.isBlank()) {
use(s);
}
Scope of s is the block where pattern matches and additional conditions hold.
Switch expressions
String label = switch (day) {
case MONDAY, FRIDAY -> "busy";
case SATURDAY, SUNDAY -> "rest";
default -> "normal";
};
yield in block form:
int hours = switch (day) {
case MONDAY -> 8;
case TUESDAY -> {
log("training");
yield 6;
}
default -> 8;
};
Switch is an expression — must produce a value or throw on all paths.
Pattern matching for switch (Java 21)
Switch on type with guards:
static double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.w() * r.h();
case Triangle t -> t.area(); // custom class
};
}
Compiler checks exhaustiveness for sealed Shape — missing case is a compile error.
Null handling: pass null → NullPointerException unless you add case null -> ....
Record patterns
Destructure nested records in one step:
static void print(Object o) {
if (o instanceof Point(int x, int y)) {
System.out.println(x + y);
}
}
static String describe(Object o) {
return switch (o) {
case Point(int x, int y) when x == y -> "on diagonal";
case Point(int x, int y) -> x + "," + y;
default -> "unknown";
};
}
Works with nested patterns: case Line(Point(int x1, int y1), Point(int x2, int y2)).
Combining sealed + records + switch
public sealed interface Expr permits Constant, Add, Neg {}
public record Constant(int value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}
public record Neg(Expr inner) implements Expr {}
static int eval(Expr e) {
return switch (e) {
case Constant(int v) -> v;
case Add(var l, var r) -> eval(l) + eval(r);
case Neg(var inner) -> -eval(inner);
};
}
This replaces visitor boilerplate for many compiler/DSL-style trees.
Common mistakes
- Mutable components in records — allowed but breaks value semantics if mutated after construction.
- Forgetting permits clause — sealed type and subclasses must agree in same module/package rules.
- Non-exhaustive switch — adding a new permitted subtype breaks compile until switch updated (feature, not bug).
- Record implements entity with setters — wrong tool; use a class.
- Using
defaulton sealed switch — hides missing cases; omit when exhaustiveness is the goal.
Practice checkpoint
-
Add validation: email record must contain
@— where? Answer: compact constructor:if (!email.contains("@")) throw .... -
Sealed
Payment permits Card, Cash— canclass Wire implements Paymentcompile? Answer: no — not in permits list (same module/package rules apply). -
switch (obj) { case String s -> ... }— obj is null? Answer: NPE unlesscase nullhandled. -
Record
Point(int x,int y)— equals uses? Answer: generated equals over all components.
Interview angles
- Record vs Lombok
@Value: built-in, serializable caveats, no bytecode plugin. - Sealed + switch: algebraic data types in Java; compiler-checked exhaustiveness.
- When not records: mutable JPA entities, inheritance-heavy frameworks, sensitive security tokens in
toString. - Pattern matching evolution: instanceof → switch type patterns → record deconstruction.