Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 26 – Advanced language and platform features

Data-oriented programming with records

Model data as sealed records, process it with exhaustive switches, nested record patterns and guards, and compare the design with classic object orientation.

  • Advanced
  • 25 minutes
  • Examples run with Java 25.0.4.1 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Model data with records and sealed interfaces, and refuse invalid values in compact constructors
  • Write exhaustive switches over sealed data with nested record patterns, guards and unnamed patterns
  • Decide when a switch over sealed data should have a default branch and when it must not
  • Compare data-oriented and classic object-oriented designs by what a new type or a new operation costs

Before you start

What you’ll learn · Before you start

What you will learn

  • Model data with records and sealed interfaces, and refuse invalid values in compact constructors
  • Write exhaustive switches over sealed data with nested record patterns, guards and unnamed patterns
  • Decide when a switch over sealed data should have a default branch and when it must not
  • Compare data-oriented and classic object-oriented designs by what a new type or a new operation costs

Before you start

On this page

Data-oriented programming keeps a program’s data and its behaviour apart. The data is a set of plain, immutable records, and when a value can take one of several shapes, a sealed interface lists the shapes. The behaviour lives in ordinary methods that take the data apart with record patterns in a switch, and javac checks that every such switch handles every shape. The style suits data that many parts of a program read and that has a fixed set of forms: messages, events, responses from another service, configuration and syntax trees. Everything it needs is standard Java: records since Java 16 (JEP 395), sealed interfaces since Java 17 (JEP 409), record patterns and pattern matching for switch since Java 21 (JEPs 440 and 441), and the unnamed pattern _ since Java 22 (JEP 456). Brian Goetz, the Java language architect, described the approach for Java in an article listed in the references.

This lesson builds a small expression language from five records and three operations, simplifies expressions with nested patterns, replaces a response class whose fields can contradict its status, and lets the compiler show what a new type and a new operation cost in each design. Every session was recorded with Eclipse Temurin 25.0.4.1, and one change was compiled again with JDK 27.

The data first: an expression language in five records

Expr is a sealed interface with five records: a number, a variable, a sum, a product and a negation. They hold data and nothing else, and because they share Expr.java with the interface, javac permits exactly these five without a permits clause. All the behaviour is in Calc, as static methods that each switch over the records:

Five records, three operations and a program that uses them

expr/Expr.java

/**
 * A small arithmetic language as data: a sealed interface and five records, with no behaviour at all. The records sit
 * in this file, so the interface needs no permits clause: javac permits exactly these five.
 */
sealed interface Expr {}

record Num(int value) implements Expr {}
record Var(String name) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Mul(Expr left, Expr right) implements Expr {}
record Neg(Expr operand) implements Expr {}

expr/Calc.java

import java.util.Map;

/** The operations on Expr: each one a switch with a case for every record, and no default. */
final class Calc {
    private Calc() {}

    /** The value of an expression, with the value of each variable taken from env. */
    static int eval(Expr e, Map<String, Integer> env) {
        return switch (e) {
            case Num(int value) -> value;
            case Var(String name) -> lookup(name, env);
            case Add(Expr left, Expr right) -> eval(left, env) + eval(right, env);
            case Mul(Expr left, Expr right) -> eval(left, env) * eval(right, env);
            case Neg(Expr operand) -> -eval(operand, env);
        };
    }

    /** The expression with every bracket written out, such as ((1 * x) + 0). */
    static String text(Expr e) {
        return switch (e) {
            case Num(int value) -> Integer.toString(value);
            case Var(String name) -> name;
            case Add(Expr left, Expr right) -> "(" + text(left) + " + " + text(right) + ")";
            case Mul(Expr left, Expr right) -> "(" + text(left) + " * " + text(right) + ")";
            case Neg(Expr operand) -> "(-" + text(operand) + ")";
        };
    }

    /** Simplifies the children first, then tries one rule on the node itself. */
    static Expr simplify(Expr e) {
        return switch (e) {
            case Num _, Var _ -> e;
            case Add(Expr left, Expr right) -> rule(new Add(simplify(left), simplify(right)));
            case Mul(Expr left, Expr right) -> rule(new Mul(simplify(left), simplify(right)));
            case Neg(Expr operand) -> rule(new Neg(simplify(operand)));
        };
    }

    /** The rewrite rules: nested record patterns, some with a guard. Most nodes match none and stay as they are. */
    private static Expr rule(Expr e) {
        return switch (e) {
            case Add(Num(int a), Num(int b)) -> new Num(a + b);
            case Mul(Num(int a), Num(int b)) -> new Num(a * b);
            case Add(Expr x, Num(int n)) when n == 0 -> x;
            case Add(Num(int n), Expr x) when n == 0 -> x;
            case Mul(Expr x, Num(int n)) when n == 1 -> x;
            case Mul(Num(int n), Expr x) when n == 1 -> x;
            case Mul(_, Num(int n)) when n == 0 -> new Num(0);
            case Mul(Num(int n), _) when n == 0 -> new Num(0);
            case Neg(Num(int value)) -> new Num(-value);
            case Neg(Neg(Expr x)) -> x;
            default -> e;
        };
    }

    private static int lookup(String name, Map<String, Integer> env) {
        Integer value = env.get(name);
        if (value == null) throw new IllegalArgumentException("no value for " + name);
        return value;
    }
}

expr/Main.java

/** Four expressions: each one printed, simplified and evaluated with x = 5 and y = 7. */
void main() {
    var x = new Var("x");
    var y = new Var("y");
    List<Expr> expressions = List.of(
            new Add(new Mul(new Num(1), x), new Num(0)),
            new Mul(y, new Add(new Num(2), new Num(-2))),
            new Neg(new Neg(new Mul(x, new Num(3)))),
            new Mul(new Add(new Num(2), new Num(3)), new Neg(new Num(4))));
    Map<String, Integer> env = Map.of("x", 5, "y", 7);
    for (Expr e : expressions) {
        Expr simple = Calc.simplify(e);
        IO.println("%-18s simplifies to %-9s value %d".formatted(Calc.text(e), Calc.text(simple), Calc.eval(simple, env)));
    }
}
Four expressions, printed, simplified and evaluated Python · expr_session.py
from session import run

# The expression language: compiled with javac, because the five records share Expr.java with the interface, then four
# expressions printed, simplified and evaluated with x = 5 and y = 7.
run("""
javac -d out expr/Expr.java expr/Calc.java expr/Main.java
java -cp out Main
""")

Output

# JDK in use: openjdk version "25.0.4.1" LTS
$ javac -d out expr/Expr.java expr/Calc.java expr/Main.java
$ java -cp out Main
((1 * x) + 0)      simplifies to x         value 5
(y * (2 + -2))     simplifies to 0         value 0
(-(-(x * 3)))      simplifies to (x * 3)   value 15
((2 + 3) * (-4))   simplifies to -20       value -20

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 expr_session.py

eval, text and simplify each have one case per record and no default, so the compiler has confirmed that all three handle all five shapes. A record pattern such as Add(Expr left, Expr right) does two jobs in one step: it tests that the value is an Add, and it binds the record’s components to new variables, in the order the record declares them. The names of those variables are your own choice; only the order and the types have to fit the record.

simplify works from the leaves up. It simplifies the children of a node first, then passes the rebuilt node to rule, which tries one rewrite at the top. That is how (y * (2 + -2)) became 0 in the run: the inner sum folded to Num(0) first, and the product with zero then became zero too. Main.java itself is a compact source file with an instance main method, which is standard since Java 25 (JEP 512).

How a nested pattern takes a tree apart

The cases of rule put patterns inside patterns. Add(Num(int a), Num(int b)) matches only a sum whose two children are both numbers, and binds both values. Mul(Expr x, Num(int n)) when n == 1 matches a product whose right child is a number, then tests that number in a guard, the condition after when. Written without patterns, the same test takes two instanceof checks, two casts and three accessor calls. The diagram follows the first expression of the run through both rules it needs:

simplify() rewrites ((1 * x) + 0) in two steps: Mul(Num(1), x) becomes x, then Add(x, Num(0)) becomes x.The valueAdd(Mul(Num(1), Var("x")), Num(0))printed as ((1 * x) + 0)Step 1, the left childMul(Num(1), Var("x"))matches case Mul(Num(int n), Expr x)when n == 1with n = 1 and x = Var("x")Step 2, the node itselfAdd(Var("x"), Num(0))matches case Add(Expr x, Num(int n))when n == 0with x = Var("x") and n = 0The resultVar("x"), printed as xsimplify the children firstthe rule returns xthe rule returns x

How simplify() reduces ((1 * x) + 0) to x

Text description of the diagram

The diagram is a column of four boxes joined by arrows.

  1. The value: Add(Mul(Num(1), Var("x")), Num(0)), which text() prints as ((1 * x) + 0).
  2. An arrow labelled "simplify the children first" leads to step 1. The left child, Mul(Num(1), Var("x")), matches the case Mul(Num(int n), Expr x) when n == 1. The pattern binds n to 1 and x to Var("x"), the guard holds, and the rule returns x.
  3. An arrow labelled "the rule returns x" leads to step 2. The node is now Add(Var("x"), Num(0)). It matches the case Add(Expr x, Num(int n)) when n == 0, which binds x to Var("x") and n to 0. The guard holds, and the rule returns x again.
  4. The last arrow leads to the result, Var("x"), which text() prints as x.

Each step is one switch case: a record pattern that tests the shape of the node and names its parts in one go, and a guard that tests a value the pattern has just bound.

Four habits of rule and simplify are worth copying:

  • _ is the unnamed pattern. Mul(_, Num(int n)) matches any left child without naming it, and case Num _, Var _ -> e in simplify gives two shapes the same answer in one case.
  • Cases are tried from the top, and the first one that applies wins. A guarded case must come before an unguarded case with the same pattern. In the opposite order the unguarded case would win every time, so javac refuses to compile the guarded one and calls it dominated.
  • A guard may use the variables its pattern has just bound, such as n, and it must be a boolean condition.
  • rule is the only switch here with a default, and that is deliberate. It is a list of rewrites for a few shapes, and every other node comes back unchanged, so a default is the honest answer. eval, text and simplify must answer for every record, and leaving the default out is what lets javac check that they do.

Illegal states that cannot be built

Data-oriented design also changes the shape of a model. StatusResponse is how many clients of a payment gateway begin: one class for every outcome, a status string, and fields that only some statuses fill in. Nothing in the type forbids a pending response with a reference, and nothing stops code from reading the reference of a payment that is still pending. The second version gives each outcome its own record:

A response class with a status string, and the same answers as sealed records

classic/StatusResponse.java · status/classic/StatusResponse.java

/**
 * The classic shape of a payment gateway's answer: one class for every outcome, a status string, and fields that only
 * some statuses fill in. Nothing stops a PENDING response with a reference, or a PAID one without.
 */
final class StatusResponse {
    final String status;    // "PAID", "DECLINED" or "PENDING"
    final String reference; // set only when PAID
    final long paise;       // set only when PAID
    final String reason;    // set only when DECLINED

    StatusResponse(String status, String reference, long paise, String reason) {
        this.status = status;
        this.reference = reference;
        this.paise = paise;
        this.reason = reason;
    }
}

classic/ClassicReceipts.java · status/classic/ClassicReceipts.java

/** A receipt for each answer. The code compiles, and the third answer gets a receipt it should never have had. */
public class ClassicReceipts {
    static String receipt(StatusResponse r) {
        if (r.status.equals("DECLINED")) return "declined: " + r.reason;
        return "receipt " + r.reference + " for Rs " + r.paise / 100;
    }

    public static void main(String[] args) {
        StatusResponse[] answers = {
            new StatusResponse("PAID", "4257120938", 1_250_00, null),
            new StatusResponse("DECLINED", null, 0, "insufficient funds"),
            new StatusResponse("PENDING", null, 0, null),
        };
        for (StatusResponse r : answers) System.out.println(receipt(r));
    }
}

status/PaymentStatus.java

/**
 * The same answers as data: one record per outcome, each with exactly the data that outcome has. A Paid without a
 * reference, or with an amount of zero or less, cannot be built: the compact constructor refuses it.
 */
sealed interface PaymentStatus {}

record Paid(String reference, long paise) implements PaymentStatus {
    Paid {
        if (reference == null || reference.isBlank()) throw new IllegalArgumentException("a paid status needs a reference");
        if (paise <= 0) throw new IllegalArgumentException("a paid amount must be positive, not " + paise);
    }
}

record Declined(String reason) implements PaymentStatus {}

record Pending(int minutes) implements PaymentStatus {}

status/Receipts.java

/** One message per status, chosen by record patterns and guards, in a switch without a default. */
public class Receipts {
    /** Receipts from this amount on are also sent by email (a rule of this example shop): Rs 2,00,000.00 in paise. */
    static final long EMAIL_FROM = 2_00_000_00;

    static String message(PaymentStatus status) {
        return switch (status) {
            case Paid(String reference, long paise) when paise >= EMAIL_FROM -> "receipt " + reference + " for " + rupees(paise) + ", also sent by email";
            case Paid(String reference, long paise) -> "receipt " + reference + " for " + rupees(paise);
            case Declined(String reason) -> "declined: " + reason;
            case Pending(int minutes) when minutes >= 30 -> "pending for " + minutes + " minutes: ask the bank, no receipt yet";
            case Pending(int minutes) -> "pending for " + minutes + " minutes: check again soon";
        };
    }

    static String rupees(long paise) {
        return "Rs %d.%02d".formatted(paise / 100, paise % 100);
    }

    public static void main(String[] args) {
        PaymentStatus[] answers = {
            new Paid("4257120938", 1_250_00), new Paid("4257120977", 2_40_000_00),
            new Declined("insufficient funds"), new Pending(4), new Pending(45),
        };
        for (PaymentStatus s : answers) System.out.println(message(s));
        try {
            new Paid(" ", 500_00);
        } catch (IllegalArgumentException e) {
            System.out.println("refused at the boundary: " + e.getMessage());
        }
    }
}
Receipts from both models Python · status_session.py
from session import run

# The same three gateway answers twice: as one class with a status string, then as sealed records with guarded cases.
run("""
javac -d classic status/classic/StatusResponse.java status/classic/ClassicReceipts.java
java -cp classic ClassicReceipts
javac -d records status/PaymentStatus.java status/Receipts.java
java -cp records Receipts
""")

Output

# JDK in use: openjdk version "25.0.4.1" LTS
$ javac -d classic status/classic/StatusResponse.java status/classic/ClassicReceipts.java
$ java -cp classic ClassicReceipts
receipt 4257120938 for Rs 1250
declined: insufficient funds
receipt null for Rs 0
$ javac -d records status/PaymentStatus.java status/Receipts.java
$ java -cp records Receipts
receipt 4257120938 for Rs 1250.00
receipt 4257120977 for Rs 240000.00, also sent by email
declined: insufficient funds
pending for 4 minutes: check again soon
pending for 45 minutes: ask the bank, no receipt yet
refused at the boundary: a paid status needs a reference

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 status_session.py

The classic code compiled without a warning and printed receipt null for Rs 0 for a payment that had not happened. In the second model a Pending has minutes and no reference, so no code can print a receipt reference for it, and the switch has to say what every outcome means. The compact constructor of Paid checks its data once, when the record is created at the edge of the program, where the gateway’s answer arrives. From then on every Paid has a reference and a positive amount, and no other code has to check again; the last line of the run shows a blank reference refused at that boundary. The guards carry the shop’s own rules, an email for large amounts and a question to the bank after half an hour, as separate cases that read like the rules themselves.

A new type or a new operation?

Each design makes some changes cheap and others expensive. To see which, the session below makes four changes to the expression language and compiles each one on its own: a new type, Div, and a new operation, size. It makes them first in a classic version, where every record carries its own eval and text methods, then in the data-oriented version above:

The classic version, and the four changes

oo/Expr.java · change/oo/Expr.java

import java.util.Map;

/** The same language written the classic way: every record carries its own eval and text. */
interface Expr {
    int eval(Map<String, Integer> env);
    String text();
}

record Num(int value) implements Expr {
    public int eval(Map<String, Integer> env) { return value; }
    public String text() { return Integer.toString(value); }
}

record Var(String name) implements Expr {
    public int eval(Map<String, Integer> env) { return env.get(name); }
    public String text() { return name; }
}

record Add(Expr left, Expr right) implements Expr {
    public int eval(Map<String, Integer> env) { return left.eval(env) + right.eval(env); }
    public String text() { return "(" + left.text() + " + " + right.text() + ")"; }
}

record Mul(Expr left, Expr right) implements Expr {
    public int eval(Map<String, Integer> env) { return left.eval(env) * right.eval(env); }
    public String text() { return "(" + left.text() + " * " + right.text() + ")"; }
}

record Neg(Expr operand) implements Expr {
    public int eval(Map<String, Integer> env) { return -operand.eval(env); }
    public String text() { return "(-" + operand.text() + ")"; }
}

oo/Div.java · change/oo/Div.java

import java.util.Map;

/** A new type: one more record with its own methods. Nothing else has to change. */
record Div(Expr left, Expr right) implements Expr {
    public int eval(Map<String, Integer> env) { return left.eval(env) / right.eval(env); }
    public String text() { return "(" + left.text() + " / " + right.text() + ")"; }
}

oo-more/Expr.java · change/oo-more/Expr.java

import java.util.Map;

/** The same language with a third operation, size, declared in the interface. */
interface Expr {
    int eval(Map<String, Integer> env);
    String text();
    int size();
}

record Num(int value) implements Expr {
    public int eval(Map<String, Integer> env) { return value; }
    public String text() { return Integer.toString(value); }
}

record Var(String name) implements Expr {
    public int eval(Map<String, Integer> env) { return env.get(name); }
    public String text() { return name; }
}

record Add(Expr left, Expr right) implements Expr {
    public int eval(Map<String, Integer> env) { return left.eval(env) + right.eval(env); }
    public String text() { return "(" + left.text() + " + " + right.text() + ")"; }
}

record Mul(Expr left, Expr right) implements Expr {
    public int eval(Map<String, Integer> env) { return left.eval(env) * right.eval(env); }
    public String text() { return "(" + left.text() + " * " + right.text() + ")"; }
}

record Neg(Expr operand) implements Expr {
    public int eval(Map<String, Integer> env) { return -operand.eval(env); }
    public String text() { return "(-" + operand.text() + ")"; }
}

dop/Size.java · change/dop/Size.java

/** A new operation: the number of nodes in an expression. Nothing else has to change. */
final class Size {
    private Size() {}

    static int of(Expr e) {
        return switch (e) {
            case Num _, Var _ -> 1;
            case Add(Expr left, Expr right) -> 1 + of(left) + of(right);
            case Mul(Expr left, Expr right) -> 1 + of(left) + of(right);
            case Neg(Expr operand) -> 1 + of(operand);
        };
    }
}

dop/Expr.java · change/dop/Expr.java

/** The same language with a sixth record, Div. */
sealed interface Expr {}

record Num(int value) implements Expr {}
record Var(String name) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Mul(Expr left, Expr right) implements Expr {}
record Neg(Expr operand) implements Expr {}
record Div(Expr left, Expr right) implements Expr {}
What javac says about each change Python · change_session.py
from session import run

# Four changes, each compiled on its own: a new type and a new operation, in the classic design (methods in every record)
# and in the data-oriented one (switches over sealed records). Two of them are meant to fail. The last change is
# compiled once more with JDK 27, whose javac also names what is missing.
run("""
# classic design, new type: Div is one more record with its own methods
javac -d a change/oo/Expr.java change/oo/Div.java
# classic design, new operation: size() declared in the interface
javac -d b change/oo-more/Expr.java
# data-oriented design, new operation: one more method with a switch
javac -d c expr/Expr.java expr/Calc.java change/dop/Size.java
# data-oriented design, new type: Div added to the sealed interface
javac -d d change/dop/Expr.java expr/Calc.java
""", expect_failure=("javac -d b change/oo-more/Expr.java", "javac -d d change/dop/Expr.java expr/Calc.java"))
run("""
javac -d d change/dop/Expr.java expr/Calc.java
""", jdk="27", expect_failure=("javac -d d change/dop/Expr.java expr/Calc.java",))

Output

# JDK in use: openjdk version "25.0.4.1" LTS
# classic design, new type: Div is one more record with its own methods
$ javac -d a change/oo/Expr.java change/oo/Div.java
# classic design, new operation: size() declared in the interface
$ javac -d b change/oo-more/Expr.java
change/oo-more/Expr.java:10: error: Num is not abstract and does not override abstract method size() in Expr
record Num(int value) implements Expr {
^
change/oo-more/Expr.java:15: error: Var is not abstract and does not override abstract method size() in Expr
record Var(String name) implements Expr {
^
change/oo-more/Expr.java:20: error: Add is not abstract and does not override abstract method size() in Expr
record Add(Expr left, Expr right) implements Expr {
^
change/oo-more/Expr.java:25: error: Mul is not abstract and does not override abstract method size() in Expr
record Mul(Expr left, Expr right) implements Expr {
^
change/oo-more/Expr.java:30: error: Neg is not abstract and does not override abstract method size() in Expr
record Neg(Expr operand) implements Expr {
^
5 errors
[exit status 1]
# data-oriented design, new operation: one more method with a switch
$ javac -d c expr/Expr.java expr/Calc.java change/dop/Size.java
# data-oriented design, new type: Div added to the sealed interface
$ javac -d d change/dop/Expr.java expr/Calc.java
expr/Calc.java:9: error: the switch expression does not cover all possible input values
        return switch (e) {
               ^
expr/Calc.java:20: error: the switch expression does not cover all possible input values
        return switch (e) {
               ^
expr/Calc.java:31: error: the switch expression does not cover all possible input values
        return switch (e) {
               ^
3 errors
[exit status 1]
# JDK in use: openjdk version "27"
$ javac -d d change/dop/Expr.java expr/Calc.java
expr/Calc.java:9: error: the switch expression does not cover all possible input values
        return switch (e) {
               ^
  missing patterns:
      Div _
expr/Calc.java:20: error: the switch expression does not cover all possible input values
        return switch (e) {
               ^
  missing patterns:
      Div _
expr/Calc.java:31: error: the switch expression does not cover all possible input values
        return switch (e) {
               ^
  missing patterns:
      Div _
3 errors
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 change_session.py

The two designs mirror each other. In the classic one, the new type was one more record and compiled at once, while the new operation in the interface broke all five records, one error each. In the data-oriented one, the new operation was one more method, while the new record made javac report the three switches of Calc that had no case for it: eval at line 9, text at line 20 and simplify at line 31. rule was not reported, because its default already covers Div, which is what a list of optional rewrites should do. On JDK 27, javac also names what each switch misses, Div _.

Neither design escapes the compiler. They put the work in different places, a trade-off that Philip Wadler named the expression problem: one design makes new types cheap to add, the other makes new operations cheap.

Methods in every class, or records and switches?
Criterion Methods in every classRecords and switches
Code of one operation In every classIn one method
New type One new classjavac lists the switches to update
New operation javac lists the classes to updateOne new method
Who adds types Anyone, if the interface is openOnly the owners
When to choose Many types, few operations: plugins, widgetsFixed shapes, many operations: events, messages, syntax trees

Most programs need both. Behaviour that belongs to one type and protects its state, such as an account that guards its balance, stays a method of that type. Data that crosses boundaries and that many parts of a program read is where records and switches pay off.

Version note

Records became final in Java 16 (JEP 395), sealed classes in Java 17 (JEP 409), record patterns and pattern matching for switch in Java 21 (JEPs 440 and 441), and unnamed patterns and variables in Java 22 (JEP 456). The records, Calc and the receipts therefore compile on Java 22 and later, and on Java 21 once each _ has a name; Main.java needs Java 25 for its compact form.

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 record Div(Expr left, Expr right) implements Expr {} is added to Expr.java. Which methods of Calc does javac report?

    Choose one answer.

    Show the answer to question 1

    Answer: eval, text and simplify, the three switches without a default

    A switch over a sealed type without a default must have a case for every permitted record, so the three total operations stop compiling until they say what Div means. rule() ends with a default, which already covers Div, so javac has nothing to report there.

  2. Question 2 of 5 What does rule in Calc.java return for this value?

    Read the code, then choose one answer.

    Expr e = new Mul(new Num(0), new Var("y"));
    Expr r = rule(e);
    Show the answer to question 2

    Answer: Num[value=0], from the case Mul(Num(int n), _) when n == 0

    The cases are tried from the top. Mul(Num, Num) does not match, because the right child is a Var. Mul(Num(int n), Expr x) matches, but its guard n == 1 is false, so the search goes on. Mul(_, Num(int n)) does not match, and Mul(Num(int n), _) when n == 0 matches with a true guard, so the result is Num(0).

  3. Question 3 of 5 In Receipts.message, the two Paid cases are swapped, so that the case without a guard comes first. What happens?

    Choose one answer.

    Show the answer to question 3

    Answer: javac rejects the guarded case, because a preceding case label dominates it

    An unguarded case Paid(String reference, long paise) applies to every Paid, so a guarded case with the same pattern after it could never apply. The Java Language Specification makes such a dominated case label a compile-time error rather than dead code.

  4. Question 4 of 5 Why does rule end with default -> e while eval has no default branch?

    Choose one answer.

    Show the answer to question 4

    Answer: rule rewrites only a few shapes and returns every other node unchanged, while eval must answer for every record, and without a default javac checks that it does

    A default is the honest answer for a partial list of rewrites. For a total operation it would hide the next new record behind whatever the default does, instead of making javac report the switch.

  5. Question 5 of 5 A logistics product adds a new kind of shipment event almost every sprint, partner teams write some of the event types, and the operations on events rarely change. Which design fits best?

    Choose one answer.

    Show the answer to question 5

    Answer: Event classes that carry their own behaviour behind an open interface

    New types are the frequent change, and partners must be able to add them, which a sealed interface would forbid. Methods on each event class make a new type one new class. Sealed records fit when the set of shapes is fixed and the operations keep growing.

Your turn: a printer with fewer brackets

text writes every bracket, which is safe but hard to read. Write Printer.show, which writes only the brackets that precedence needs, following three rules:

  1. A Num prints its value, in brackets when it is negative, as in (-3). A Var prints its name.
  2. An Add prints left + right, a Mul prints left * right, and a Neg prints -operand.
  3. A child gets brackets when it binds more loosely than its parent. Neg binds most tightly, then Mul, then Add, so an Add inside a Mul or a Neg gets brackets, and so does a Mul inside a Neg.
The starter code and its sample checks

printer/Printer.java

/** Your turn: print an expression with only the brackets that precedence needs. */
final class Printer {
    private Printer() {}

    /** Neg binds most tightly, then Mul, then Add (the rules are in the lesson). */
    static String show(Expr e) {
        return ""; // replace this line with one switch over the five records
    }
}

printer/Checks.java

/** Sample checks for Printer.show: javac -d out expr/Expr.java printer/*.java, then java -cp out Checks */
public class Checks {
    private static int passed;
    private static int total;

    public static void main(String[] args) {
        var x = new Var("x");
        check("7", new Num(7));
        check("x + 2 * 3", new Add(x, new Mul(new Num(2), new Num(3))));
        check("(x + 2) * 3", new Mul(new Add(x, new Num(2)), new Num(3)));
        check("-(x + 2)", new Neg(new Add(x, new Num(2))));
        check("-x * 3", new Mul(new Neg(x), new Num(3)));
        check("-(-3)", new Neg(new Num(-3)));
        System.out.println(passed + " of " + total + " checks passed");
    }

    private static void check(String expected, Expr e) {
        total++;
        String got = Printer.show(e);
        if (got.equals(expected)) passed++;
        else System.out.println("expected \"" + expected + "\" but got \"" + got + "\"");
    }
}
The sample checks against the starter code Python · exercise_session.py
from session import run

# The exercise's sample checks against the starter code, which returns an empty string for every expression.
run("""
javac -d out expr/Expr.java printer/Printer.java printer/Checks.java
java -cp out Checks
""")

Output

# JDK in use: openjdk version "25.0.4.1" LTS
$ javac -d out expr/Expr.java printer/Printer.java printer/Checks.java
$ java -cp out Checks
expected "7" but got ""
expected "x + 2 * 3" but got ""
expected "(x + 2) * 3" but got ""
expected "-(x + 2)" but got ""
expected "-x * 3" but got ""
expected "-(-3)" but got ""
0 of 6 checks passed

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exercise_session.py

  1. Write show as one switch over the five records, with no default. A small helper that gives each record a precedence level keeps the third rule short.
  2. Compile with javac -d out expr/Expr.java printer/*.java and run java -cp out Checks until it prints 6 of 6 checks passed.
  3. Add record Sub(Expr left, Expr right) implements Expr {} to Expr.java and compile everything with javac -d out expr/*.java printer/*.java. Read which switches javac reports before you change anything. Then print a subtraction as left - right, with brackets around a right child that is an Add or a Sub, as in x - (2 + 3).

Interview questions

Fresher · What is data-oriented programming in Java, and which language features support it?

Data-oriented programming models a program’s data as plain, immutable values and keeps the code that works on them in separate methods. In Java, records hold the data, and a sealed interface lists the shapes a value can take, such as the outcomes of a payment. Methods take values apart with record patterns in switch expressions; because the interface is sealed, a switch without a default must handle every shape, and javac reports each switch that has to change when a shape is added. Guards add a condition to a case, and the unnamed pattern ignores the parts that do not matter. Compact constructors check data once, when it enters the program, so invalid values cannot be built. The style suits data with a fixed set of forms and many operations, such as messages, events and syntax trees, while behaviour that belongs to one type stays a method of that type.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    An order service models 14 kinds of order event as subclasses with a visitor interface, and every new event touches nine visitor classes. A colleague proposes sealed records with pattern switches instead. How would you decide, and how would you migrate safely?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A service returns a Result class with the fields status, data and errorMessage. Every few weeks a bug reads data when status is FAILED and shows an empty screen or throws NullPointerException. How would you remodel the result with Java 25 features, and how do the callers change?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Records hold the data, a sealed interface lists its shapes, and operations are methods that switch over the shapes with record patterns.
  • Nested record patterns test the shape of a whole subtree and bind its parts in one case; guards add a condition, and _ ignores what does not matter.
  • Leave the default out of a switch that must answer for every shape, so javac checks it; keep a default where only some shapes need special handling.
  • Give each outcome a record with exactly its own data, and validate it in the compact constructor, so contradictory states cannot be built.
  • A new record makes javac list every switch to update, and a new operation is one method: the mirror image of classic object-oriented design, where new types are cheap and new operations touch every class.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.