Java Arena
Step 4 of 4

Record patterns

A type pattern gives you the whole object, and you then read its values with accessors. Since Java 21, a pattern can also take a record apart: a record pattern names the record and, in parentheses, a pattern for each of its components.

Taking a record apart

Main.java
public class Main {
    public static void main(String[] args) {
        System.out.println(receipt(new Cash(450)));
        System.out.println(receipt(new BankCard("4921", 1250)));
        System.out.println(receipt(new Voucher("SPRING10", 1000)));
    }

    public static String receipt(Payment payment) {
        return switch (payment) {
            case Cash(int cents) -> "Cash: " + cents + " cents";
            case BankCard(String digits, int cents) -> "Card ending " + digits + ": " + cents + " cents";
            case Voucher(String code, int cents) -> "Voucher " + code + ": " + cents + " cents";
        };
    }
}
Payment.java
public sealed interface Payment permits Cash, BankCard, Voucher {
}
Cash.java
public record Cash(int cents) implements Payment {
}
BankCard.java
public record BankCard(String lastDigits, int cents) implements Payment {
}
Voucher.java
public record Voucher(String code, int cents) implements Payment {
}
Output
Cash: 450 cents
Card ending 4921: 1250 cents
Voucher SPRING10: 1000 cents

BankCard(String digits, int cents) matches a BankCard, and declares a variable for each component, filled in from the accessors: digits gets the value of lastDigits(), and cents of cents().

  • The components are matched by their position, not by name: the first variable gets the first component. The names are yours to choose, as digits shows.
  • The types must fit the components' types. In place of a type, var (module 57) lets javac take the component's own type: case BankCard(var digits, var cents).
  • Every component gets a pattern, even one the case doesn't use.

Record patterns work after instanceof too, and can be combined with && like type patterns:

Main.java
public class Main {
    public static void main(String[] args) {
        Payment[] today = {new Voucher("SPRING10", 1000), new Cash(450), new Voucher("BIG50", 5000)};
        for (Payment payment : today) {
            if (payment instanceof Voucher(String code, int cents) && cents >= 5000) {
                System.out.println("Check the voucher " + code + " by hand");
            }
        }
    }
}
Payment.java
public sealed interface Payment permits Cash, BankCard, Voucher {
}
Cash.java
public record Cash(int cents) implements Payment {
}
BankCard.java
public record BankCard(String lastDigits, int cents) implements Payment {
}
Voucher.java
public record Voucher(String code, int cents) implements Payment {
}
Output
Check the voucher BIG50 by hand

In a switch, a when guard can use the pattern's variables the same way. The guarded case comes before the plain one, so a big voucher stops at the first case:

Main.java
public class Main {
    public static void main(String[] args) {
        Payment[] today = {new Voucher("SPRING10", 1000), new Cash(450), new Voucher("BIG50", 5000)};
        for (Payment payment : today) {
            System.out.println(describe(payment));
        }
    }

    public static String describe(Payment payment) {
        return switch (payment) {
            case Voucher(String code, int cents) when cents >= 5000 -> "Check the voucher " + code + " by hand";
            case Voucher(String code, int cents) -> "Voucher " + code;
            case Cash(int cents) -> "Cash";
            case BankCard(String digits, int cents) -> "Card " + digits;
        };
    }
}
Payment.java
public sealed interface Payment permits Cash, BankCard, Voucher {
}
Cash.java
public record Cash(int cents) implements Payment {
}
BankCard.java
public record BankCard(String lastDigits, int cents) implements Payment {
}
Voucher.java
public record Voucher(String code, int cents) implements Payment {
}
Output
Voucher SPRING10
Cash
Check the voucher BIG50 by hand

Patterns inside patterns

A component's pattern can itself be a record pattern or a type pattern. A café order holds the item and its payment, and one switch can look at both at once:

Main.java
public class Main {
    public static void main(String[] args) {
        System.out.println(note(new Order("cake", new Voucher("SPRING10", 1000))));
        System.out.println(note(new Order("tea", new BankCard("4921", 320))));
        System.out.println(note(new Order("bun", new Cash(250))));
    }

    public static String note(Order order) {
        return switch (order) {
            case Order(String item, Voucher(String code, int cents)) -> item + " paid with voucher " + code;
            case Order(String item, BankCard(var digits, var cents)) -> item + " paid with card " + digits;
            case Order(String item, Payment other) -> item + " paid";
        };
    }
}
Order.java
public record Order(String item, Payment payment) {
}
Payment.java
public sealed interface Payment permits Cash, BankCard, Voucher {
}
Cash.java
public record Cash(int cents) implements Payment {
}
BankCard.java
public record BankCard(String lastDigits, int cents) implements Payment {
}
Voucher.java
public record Voucher(String code, int cents) implements Payment {
}
Output
cake paid with voucher SPRING10
tea paid with card 4921
bun paid

The last case, Order(String item, Payment other), matches every order, so the switch is exhaustive.

Records that hold their own kind

A record of a sealed interface can have a component of that same interface. Then one object can hold another, which holds another, and a method that takes them apart calls itself (module 55). A present can be a gift, or something wrapped in paper around another present:

Main.java
public class Main {
    public static void main(String[] args) {
        Present box = new Wrapped("red", new Wrapped("gold", new Gift("a scarf")));
        System.out.println(unwrap(box));
    }

    public static String unwrap(Present present) {
        return switch (present) {
            case Gift(String name) -> name;
            case Wrapped(String paper, Present inside) -> paper + " paper, then " + unwrap(inside);
        };
    }
}
Present.java
public sealed interface Present permits Gift, Wrapped {
}
Gift.java
public record Gift(String name) implements Present {
}
Wrapped.java
public record Wrapped(String paper, Present inside) implements Present {
}
Output
red paper, then gold paper, then a scarf

The base case is the Gift, which holds no other present. The last challenge of this module builds a calculator on the same idea.

Your task

Challenge 1 of 3

A floor plan file has been read into a list of objects: a String for each room's name, and a Corner(int x, int y) record for each corner of the walls, in centimeters. Write printCorners(ArrayList<Object> items), which prints each corner as corner at x, y and skips everything else. Take the corner apart with a record pattern after instanceof, without calling the accessors. The check runs:

ArrayList<Object> items = new ArrayList<>();
items.add("Kitchen");
items.add(new Corner(0, 0));
items.add(new Corner(400, 0));
items.add("Hall");
items.add(new Corner(400, 350));
printCorners(items);

and expects:

corner at 0, 0
corner at 400, 0
corner at 400, 350

Challenge 2

Each wall of the floor plan goes from one corner to another: Wall(Corner from, Corner to). Write describe(Wall wall), which returns:

  • horizontal, and the length and cm, when both corners have the same y;
  • vertical, and the length and cm, when both corners have the same x (and different ys);
  • slanted otherwise.

The length is the difference of the coordinates that change, as a positive number (Math.abs). Take the wall and both its corners apart with one nested record pattern, Wall(Corner(...), Corner(...)), in a switch or after instanceof, without calling the accessors. The check runs:

System.out.println(describe(new Wall(new Corner(0, 0), new Corner(400, 0))));
System.out.println(describe(new Wall(new Corner(400, 350), new Corner(400, 0))));
System.out.println(describe(new Wall(new Corner(0, 0), new Corner(300, 200))));

and expects:

horizontal, 400 cm
vertical, 350 cm
slanted

Challenge 3

A small calculator stores an expression such as (2 + 3) * 4 as a tree of records. An Expr is a number, Num(int value), or the sum or the product of two smaller expressions, Add(Expr left, Expr right) and Mul(Expr left, Expr right). The method show in Main.java already turns an expression into text, with a switch over record patterns that calls itself for the parts. Write eval(Expr expr) in the same way: it returns the expression's value. Use a case with a record pattern for each of the three records, and no default. The check runs:

Expr sum = new Add(new Num(2), new Num(3));
Expr product = new Mul(sum, new Num(4));
System.out.println(show(sum) + " = " + eval(sum));
System.out.println(show(product) + " = " + eval(product));

and expects:

(2 + 3) = 5
((2 + 3) * 4) = 20