Start Learning
Javaneer
Back to stage
Stage 2·Object-Oriented Programming

Records, Sealed Classes & Enums

The modern Java type system: records, sealed hierarchies, pattern matching, and enums.

18 min readIntermediate
On this page

Modern Java (16+) added concise tools that remove boilerplate and make your types safer and clearer: records, sealed classes, and enums. They help you say more with less code.

Enums: a fixed set of named values

An enum defines a type with a fixed set of possible values. Instead of using loose strings or magic numbers, you get a small, safe, self-documenting set.

An enum is like the gears in a car

A car's gearstick has a fixed set of positions: Park, Reverse, Neutral, Drive. You can't shift into "Banana". An enum is that gearstick - it constrains a value to a known, valid set, so invalid states are impossible.

enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
Day today = Day.WEDNESDAY;

String plan = switch (today) {
    case SATURDAY, SUNDAY -> "Relax";
    default -> "Work";
};
System.out.println(plan);   // Work

Enums can also carry data and methods:

enum Planet {
    EARTH(9.81), MARS(3.71);

    private final double gravity;
    Planet(double gravity) { this.gravity = gravity; }
    double weight(double mass) { return mass * gravity; }
}

Prefer enums over string constants

Using "WEDNESDAY" as a raw string invites typos ("WENSDAY") that compile fine but fail at runtime. An enum catches such mistakes at compile time and enables exhaustive switch handling. Reach for enums whenever a value has a fixed set of options.

Records: data classes without the boilerplate

Before records, a simple data holder required a constructor, getters, equals(), hashCode(), and toString() - dozens of lines for something trivial. A record generates all of that automatically.

record Point(int x, int y) { }

That single line gives you:

  • a constructor Point(int x, int y)
  • accessor methods x() and y()
  • sensible equals(), hashCode(), and toString()
  • immutability - a record's fields can't change after creation
Point p = new Point(3, 4);
System.out.println(p.x());     // 3
System.out.println(p);         // Point[x=3, y=4]

Point q = new Point(3, 4);
System.out.println(p.equals(q));  // true - value equality for free!
A record is like a printed form

A record is like a filled-out form: name here, address there - a fixed set of fields, filled once, then read many times. It's about carrying data, not behaving. When your class is really just data, make it a record.

Sealed classes: controlling who can extend

A sealed class or interface restricts which classes may extend or implement it. You explicitly list the permitted subtypes - no surprises.

sealed interface Shape permits Circle, Rectangle { }

record Circle(double radius) implements Shape { }
record Rectangle(double width, double height) implements Shape { }

Now Shape has exactly two implementations, and Java knows it. This pairs beautifully with switch pattern matching, which can be exhaustive - the compiler guarantees you've handled every case:

double area = switch (shape) {
    case Circle c    -> Math.PI * c.radius() * c.radius();
    case Rectangle r -> r.width() * r.height();
    // no 'default' needed - the compiler knows these are the only options
};

Why 'sealing' is powerful

Sealed types let you say "this is a closed set of possibilities." Combined with records and pattern matching, they make modeling data - like the results of an operation, or the shapes in a drawing app - remarkably safe and expressive. Add a new subtype, and the compiler flags every switch you forgot to update.

Putting it together

sealed interface Payment permits Cash, Card { }
record Cash(double amount) implements Payment { }
record Card(double amount, String number) implements Payment { }

String describe(Payment p) {
    return switch (p) {
        case Cash c -> "Cash: $" + c.amount();
        case Card c -> "Card ending " + c.number();
    };
}

Concise, immutable, type-safe, and exhaustive - modern Java at its best.

Quick check

What does declaring 'record Point(int x, int y) {}' give you automatically?

Key takeaways

  • Enums define a fixed, type-safe set of named values and can carry data and methods.
  • Prefer enums over raw string or int constants - mistakes are caught at compile time.
  • Records are concise, immutable data classes that auto-generate constructor, accessors, equals, hashCode, and toString.
  • Sealed classes/interfaces restrict which types may extend them via a 'permits' list.
  • Sealed types plus record patterns enable exhaustive switch - the compiler ensures every case is handled.

One more essential OOP topic remains: how objects are compared for equality, and why immutability makes everything simpler. That's next.