Core Libraries
Enums & Records
An enum is a fixed, type-safe set of named constant instances that can carry fields and methods. A record is an immutable data carrier that generates its constructor, accessors, equals, hashCode, and toString from its components — making the last chapter's contract free. And a sealed type restricts which classes may extend or implement it, so the compiler knows the whole family. Every feature shown with verified output.
Suggest an editEnums & Records — Modeling Data Precisely
Three modern features let you model data so precisely that whole categories of bug become impossible to write. An enum is a fixed set of named constants — a type whose every value you list up front, so an invalid one can't exist. A record (JDK 16) is an immutable data carrier: you declare its components and Java generates the constructor, accessors, and a correct equals/hashCode/toString for free. A sealed type (JDK 17) names exactly which classes may implement it, so the set of subtypes is closed and known. Together they replace piles of hand-written boilerplate — and the bugs that hide in it.
💡 The core idea.
- An enum is a fixed set of named constants — an invalid value can't exist.
- A record generates constructor, accessors,
equals/hashCode/toStringfrom its components. - A sealed type names exactly which subtypes may exist — a closed, known set.
- Together they replace hand-written boilerplate and the bugs hiding in it.
Every output below was produced by compiling and running the code.
📘 How to read the Intuition boxes. Each one is built in three moves:
- The mechanism — what the compiler and the JVM are actually doing.
- A concrete bite — a specific, runnable failure (often a real compiler error), shown so the trap is visible.
- The earned rule — the decision heuristic, now justified rather than asserted, plus its cost.
Table of contents
- Enums: a fixed set of constants
- Rich enums: fields and methods
- Records: immutable data carriers
- Sealed types: a first look
- Mental-model summary
- Gotcha checklist
1. Enums: a fixed set of constants
An enum declares a type whose only values are the named constants you list. Each is a singleton instance of the enum type — type-safe (you can't pass a stray string), and a natural fit for a switch.
Output:
WED
2
weekday
7Analysis. Day.WED prints its name; ordinal() is its position (2, zero-based); the switch classified it as a weekday; and Day.values() returns all seven constants. A Day variable can hold only one of these seven — there is no eighth day, and no way to assign a typo'd one, because the compiler checks every Day value against the declared set.
Intuition.
Mechanism. The compiler turns each constant into a public static final instance of the enum type, created once. A variable of the enum type can reference only those instances, so the set of valid values is closed at compile time.
Concrete bite. That closed set lets a switch expression over an enum be checked for exhaustiveness — cover some constants but not all, with no default, and it won't compile:
Compiler error:
Main.java:5: error: the switch expression does not cover all possible input values
String s = switch (d) {
^The switch handles only two of seven days and produces a value, so the compiler demands the rest (or a default). With an enum, "I forgot a case" becomes a build error.
💡 Earned rule. Use an enum for any value that comes from a fixed, known set — states, directions, days, modes — instead of int codes or Strings. The cost is declaring the type; the benefit is type safety (no invalid value), readable names, and exhaustiveness checking in switch that flags a forgotten case at compile time.
2. Rich enums: fields and methods
Enum constants are real objects, so they can carry data and behavior. Give the enum a constructor and fields, pass each constant its values in parentheses, and add methods that use them.
Output:
EARTH: 98.1
MARS: 37.1
MOON: 16.2Analysis. Each constant supplied a gravity to the enum's constructor (EARTH(9.81)), stored in a final field, and weightOf used it. So Planet isn't just three names — it's three objects each bundling a value and a method, iterated with values(). The gravity is final because enum constants are effectively immutable singletons.
Intuition.
Mechanism. EARTH(9.81) calls the enum's constructor when the constant is created (once, at class load). The fields and methods make each constant a small, self-describing object — an enum is a class whose instances are fixed and named.
Concrete bite. This replaces the fragile "parallel arrays" or switch-on-code style: instead of a double gravityFor(int planetCode) with a switch you must keep in sync, the data lives on the constant, so adding a planet adds one line and can't desync. The behavior travels with the value.
💡 Earned rule. Put per-constant data and behavior in the enum (fields, a constructor, methods) rather than in external switches keyed on the constant. The cost is a slightly richer enum declaration; the benefit is that each constant is self-contained — add or change one and there's a single place to edit, with no lookup table to keep aligned.
3. Records: immutable data carriers
A record is a class whose entire job is to hold data. You declare its components, and Java generates a canonical constructor, an accessor per component, and consistent equals, hashCode, and toString — all immutable, all for free.
Output:
Point[x=1, y=2]
1,2
true
trueAnalysis. One line — record Point(int x, int y) {} — gave us a constructor (new Point(1, 2)), accessors (a.x(), a.y() — note the parentheses), a readable toString (Point[x=1, y=2]), and a correct equals/hashCode pair: a.equals(b) is true and their hash codes match. Recall from the last chapter how much hand-written, error-prone code that contract took — a record generates it from the components, consistently, every time.
Intuition.
Mechanism. A record is a compact, final, immutable class. The compiler derives the members from the component list: private final fields, a canonical constructor that assigns them, accessors named after each component, and equals/hashCode/toString computed over all components. You can still add methods or validate in a compact constructor.
Concrete bite. The immutability is real — record components are final, with no setters, so you can't change one after construction:
Compiler error:
Main.java:5: error: x has private access in Point
p.x = 5;
^
1 errorp.x = 5 is rejected — the component is a private final field, readable only through the accessor p.x(), never assignable. A record is immutable by construction; to "change" a point you build a new one.
💡 Earned rule. Use a record for any immutable group of values — coordinates, a name/email pair, a DTO, a Map key — and let it generate the boilerplate and the equals/hashCode contract. The cost is immutability (a record can't be a mutable bean) and that it can't extend a class; the benefit is a correct, concise value type where a hand-written class would be dozens of lines of bug-prone boilerplate.
4. Sealed types: a first look
A sealed interface or class names exactly which types may implement or extend it, with a permits clause. The subtype set is then closed — the compiler knows every possible implementation, which is the foundation for the exhaustive pattern matching of Tutorial 26.
Output:
Circle[radius=2.0]Analysis. Shape permits exactly Circle and Square (both records here — records and sealed types pair naturally). The diagram shows the closed family: a Shape is a Circle or a Square, and nothing else can claim to be one. That "nothing else" is the guarantee a plain interface can't make — any class anywhere could implement an ordinary interface.
Intuition.
Mechanism. sealed … permits A, B records the allowed subtypes in the type itself; the compiler enforces that only those types extend/implement it (each must be final, sealed, or non-sealed). The subtype set is fixed and visible at compile time.
Concrete bite. A type not in the permits clause cannot join the family:
Compiler error:
Main.java:3: error: class is not allowed to extend sealed class: Shape (as it is not listed in its 'permits' clause)Triangle tried to implement Shape but isn't permitted, so it's rejected. The family stays exactly {Circle} — the seal holds.
💡 Earned rule. Seal an interface or class when the set of subtypes is meant to be closed and known — a fixed algebra of cases like shapes, AST nodes, or result variants — and pair it with records for the cases. The cost is listing the permitted types (and updating the list to add one); the benefit is a closed family the compiler can reason about, enabling the exhaustive switch pattern matching of Tutorial 26 with no default needed.
5. Mental-model summary
| Principle | Consequence |
|---|---|
| An enum is a fixed, type-safe set of named constant instances | No invalid value; exhaustive switch flags a forgotten case |
| Enum constants can carry fields and methods | Per-constant data/behavior lives on the constant, not in external switches |
A record generates constructor, accessors, equals/hashCode/toString |
An immutable value type in one line, with the contract correct for free |
Record components are final, read via x() accessors |
You can't assign them; "change" means build a new record |
A sealed type's permits clause closes its subtype set |
Only listed types may implement it; the compiler knows the whole family |
6. Gotcha checklist
the switch expression does not cover all possible input valueson an enum → add the missing constants or adefault; the closed set is exhaustiveness-checked.x has private accessassigning a record component → records are immutable; read viax(), build a new record to "change" it.- Two equal records aren't equal / break a
HashMap→ they won't — a record generates a correctequals/hashCode; that's a reason to use one. class is not allowed to extend sealed class→ add the type to thepermitsclause, or it can't join the sealed family.- Reached for
int/Stringcodes for a fixed set of values → use an enum for type safety, names, and switch exhaustiveness.
🧪 Predict, then check. Give Day a boolean isWeekend() method and predict what Day.SAT.isWeekend() and Day.MON.isWeekend() return. Next, for record Money(int cents, String currency) {}, predict the output of printing new Money(100, "USD") and comparing two equal Money values with .equals. Finally, predict whether a record Rectangle(double w, double h) implements Shape {} compiles given sealed interface Shape permits Circle, Square {} — and what one change makes it compile.
Your Turn
Before you move on, check your understanding with the coach — explain the idea, apply it, weigh the trade-offs, then defend your reasoning.