Records and Sealed Classes
|
This section documents the current Java release line, with Java 25 LTS as the reference point (no specific patch version is pinned), as published at the Java developer portal, the Java Tutorials, and the Java SE API specification — which are the references these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
A record is a concise, immutable data carrier whose API is its state; a sealed type restricts
which classes may extend or implement it, so the compiler knows the complete set of subtypes. Together
they give Java algebraic data types. This page follows the
dev.java "Using Records to Model Immutable Data" track and the JDK 25
language guides for records and
sealed classes
and interfaces.
Records
A record declares its components in the header. From record Point(int x, int y) \{ } the compiler
generates a private final field per component, a public accessor named exactly after each component
(x(), y() — no getX()), an all-arguments canonical constructor, and value-based
equals,
hashCode, and toString.
record Point(int x, int y) { }
var p = new Point(3, 4);
p.x(); // 3 -- accessor, no "get" prefix
p.equals(new Point(3, 4)); // true -- compared component by component
p.toString(); // Point[x=3, y=4]
A record is implicitly final, cannot extend another class (it already extends
java.lang.Record),
and has no instance initializer blocks and no non-static fields beyond its components. It can
implement interfaces, declare static members, and declare additional instance methods.
Compact constructor: validation and normalization
The compact constructor has no parameter list and no field assignments. It runs before the implicit assignment of every component, so it is the place to validate arguments and to normalize values by reassigning the parameters.
import java.util.Objects;
record Range(int low, int high) {
Range { // compact: no (int low, int high), no this.low = ...
if (low > high) {
throw new IllegalArgumentException("low > high: " + low + " > " + high);
}
}
}
record Name(String value) {
Name {
value = Objects.requireNonNull(value, "value").strip(); // reassigning the parameter here
} // normalizes the field that is stored
}
new Name(" Ada ").value(); // "Ada"
Extra constructors, static factories, interfaces
interface Shape { double area(); }
record Circle(double radius) implements Shape {
Circle { // compact constructor: reject bad input
if (radius <= 0) {
throw new IllegalArgumentException("radius must be > 0");
}
}
Circle() { // extra constructor: must delegate
this(1.0);
}
static Circle ofDiameter(double d) { // static factory method
return new Circle(d / 2);
}
@Override
public double area() { // additional instance method
return Math.PI * radius * radius;
}
static final Circle UNIT = new Circle(1.0); // static field is allowed
}
Every non-compact constructor in a record must eventually reach the canonical constructor through
this(…).
When a Record Fits
Reach for a record when a value is defined entirely by its data, that data never changes after
construction, and two instances with equal components should be considered equal.
// Fits a record: an immutable value, where identity == data
record Money(long amountCents, String currency) { }
// Not a record: it has an identity, mutable state, and lifecycle behaviour
final class ShoppingCart {
private final java.util.List<Money> lines = new java.util.ArrayList<>();
void add(Money line) { lines.add(line); }
java.util.List<Money> lines() { return java.util.List.copyOf(lines); }
}
-
Prefer a normal class when the object has mutable state, has an identity independent of its fields (two accounts with the same balance are not "equal"), must extend another class, or needs to hide or derive fields.
-
Prefer a record over a
Map<String, Object>or anObject[]"tuple" whenever the shape is known: you gain names, static types, immutability, and correctequals/hashCodefor free, all checked at compile time. -
A record is only shallowly immutable — a
Listcomponent can still be mutated through an outside reference. Copy defensively in the compact constructor, or storeList.copyOf(…), when that matters.
Sealed Classes and Interfaces
A sealed type lists its permitted direct subtypes after permits (the clause may be omitted when
every subtype is declared in the same source file). Each permitted subtype must itself be declared
final, sealed, or non-sealed, and — in an unnamed module — must sit in the same package; in a
named module it must sit in the same module.
sealed interface Expr permits Lit, Add, Neg { }
record Lit(double value) implements Expr { }
record Add(Expr left, Expr right) implements Expr { }
record Neg(Expr operand) implements Expr { }
final closes a branch to further subclassing; sealed keeps it restricted further down; non-sealed
re-opens it so that any code may extend that subtype.
sealed class Figure permits Disk, Polygon { }
final class Disk extends Figure { } // no further subclasses
non-sealed class Polygon extends Figure { } // re-opened: anyone may extend Polygon
class Hexagon extends Polygon { } // ...like this, with no permits entry needed
Because the compiler knows every permitted subtype, a switch over a sealed type is exhaustive
without a default once every subtype has a case — and it stops compiling when a new subtype is
added but a switch is not updated to handle it.
Records + Sealed + Pattern Matching
A switch over a sealed hierarchy of records combines exhaustiveness with record deconstruction
patterns: each case both tests the runtime type and binds the components in one step.
static double eval(Expr e) {
return switch (e) {
case Lit(double v) -> v;
case Add(Expr l, Expr r) -> eval(l) + eval(r);
case Neg(Expr operand) -> -eval(operand);
// no default: Expr is sealed and every permitted type is covered
};
}
var tree = new Add(new Lit(3), new Neg(new Lit(1)));
eval(tree); // 2.0
Nested patterns (case Add(Lit(var a), Lit(var b))) and when guards work here too. The full
treatment — instanceof patterns, unnamed patterns, guard clauses — is on
Pattern Matching.
See Also
-
Pattern Matching — type patterns, record deconstruction, and guarded
switchlabels in depth. -
Classes and Objects — the constructor, field, and
equals/hashCodemechanics a record automates. -
Interfaces —
sealed interfaceand the interfaces a record can implement. -
Enums — the other closed, exhaustively switchable type.