IntermediateJava · Lesson 2 of 9

Records, Enums, Sealed Types & Pattern Matching

Concise data classes, fixed sets of constants, closed type hierarchies and switch patterns.

A record is a compact, immutable data class: record Result(String student, int score) {} gives you a constructor, accessors (score()), equals, hashCode and toString. Add validation in a compact constructor.

An enum is a fixed set of named constants, which can have fields and methods — perfect for grades, payment methods or statuses.

A sealed interface lists exactly which types may implement it. Combined with pattern matching in switch, the compiler checks that every case is handled: add a new payment type and every switch that forgot it stops compiling.

Payments.javaJava
import java.util.List;

public class Payments {
    public static void main(String[] args) {
        List<Payment> payments = List.of(
            new MobileMoney("0712345678", 150_000),
            new BankTransfer("CRDB", "TX-88231", 300_000),
            new Cash(50_000)
        );

        long total = 0;
        for (Payment p : payments) {
            System.out.println(describe(p));
            total += p.amount();
        }
        System.out.println("Total: " + total);

        for (Grade g : Grade.values()) {
            System.out.println(g + " from " + g.minScore + ": " + g.remark);
        }
        System.out.println(Grade.forScore(68) + " " + Grade.valueOf("A").remark);

        Result r = new Result("Amina", 88);
        System.out.println(r + " " + r.score() + " " + r.equals(new Result("Amina", 88)));
        try {
            new Result("Juma", 120);
        } catch (IllegalArgumentException e) {
            System.out.println("Rejected: " + e.getMessage());
        }
    }

    static String describe(Payment p) {
        return switch (p) {
            case MobileMoney m -> "Mobile money from " + m.phone() + ": " + m.amount();
            case BankTransfer b when b.amount() >= 250_000 -> "LARGE bank transfer " + b.reference();
            case BankTransfer b -> "Bank transfer via " + b.bank() + ": " + b.amount();
            case Cash c -> "Cash: " + c.amount();
        };   // no default needed: the sealed interface lists every case
    }
}

sealed interface Payment permits MobileMoney, BankTransfer, Cash {
    long amount();
}

record MobileMoney(String phone, long amount) implements Payment {}
record BankTransfer(String bank, String reference, long amount) implements Payment {}
record Cash(long amount) implements Payment {}

record Result(String student, int score) {
    Result {                                    // compact constructor: validation
        if (score < 0 || score > 100) throw new IllegalArgumentException("score " + score);
    }
}

enum Grade {
    A(75, "Excellent"), B(65, "Very good"), C(45, "Good"), D(30, "Satisfactory"), F(0, "Fail");

    final int minScore;
    final String remark;

    Grade(int minScore, String remark) {
        this.minScore = minScore;
        this.remark = remark;
    }

    static Grade forScore(int score) {
        for (Grade g : values()) {
            if (score >= g.minScore) return g;
        }
        return F;
    }
}

Key points

  • Use records for immutable data; validate in the compact constructor.
  • Enums model fixed sets and can carry data and behaviour.
  • Sealed types + switch patterns = the compiler guarantees every case is handled.

Exercise

Model shapes as a sealed interface with Circle, Rectangle and Triangle records, and compute the area with a pattern-matching switch. Add an enum Term { ONE, TWO } and a record TermResult(String student, Term term, int score).

Show solution

Try the exercise yourself first — then compare your approach with this one.

The sealed interface lists the three shapes, so the pattern-matching switch needs no default — add a fourth shape and this code stops compiling until you handle it. The enum and record model term results with no boilerplate.

SealedShapes.javaJava
import java.util.List;

public class SealedShapes {
    sealed interface Shape permits Circle, Rectangle, Triangle {}
    record Circle(double radius) implements Shape {}
    record Rectangle(double width, double height) implements Shape {}
    record Triangle(double base, double height) implements Shape {}

    enum Term { ONE, TWO }
    record TermResult(String student, Term term, int score) {}

    static double area(Shape shape) {
        return switch (shape) {
            case Circle c -> Math.PI * c.radius() * c.radius();
            case Rectangle r -> r.width() * r.height();
            case Triangle t -> 0.5 * t.base() * t.height();
        };
    }

    public static void main(String[] args) {
        for (Shape s : List.of(new Circle(1), new Rectangle(3, 4), new Triangle(6, 2))) {
            System.out.printf("%s -> %.2f%n", s, area(s));
        }

        List<TermResult> results = List.of(
            new TermResult("Amina", Term.ONE, 86), new TermResult("Amina", Term.TWO, 89));
        for (TermResult r : results) {
            System.out.println(r.student() + " term " + r.term() + ": " + r.score());
        }
    }
}

Check your understanding

  1. What does a record generate automatically?

  2. Where should a record validate its values?

  3. Why doesn't a switch over a sealed interface need a default case?

  4. What can an enum constant carry, as in A(75, "Excellent")?

Ask AI