Java - Record Patterns Examples

This tutorial explains how to use the record patterns feature in Java.

Java 14 introduced record which is a special kind of class that can be declared with less ceremony than a conventional class. In Java 19, there is a new preview feature called record patterns that makes it more comfortable to use record. The feature enables you to deconstruct the record fields directly. You can read the examples below to get better understanding.

instanceOf Usage

The instanceof keyword in Java can be used to check whether a variable is an instance of a class. It also works for checking whether a variable is an instance of a record.

For example, we have a record named Point with two fields x and y.

  record Point(double x, double y) {}

There is a variable whose type is Object. If the Object is an instance of Point, we want to get the sum of the x and y. Without record patterns, we have to cast the Object to Point first.

  Double getSum(Object obj) {
    if (obj instanceof Point) {
      Point point = (Point) obj;
      return point.x + point.y;
    }

    return null;
  }

With record patterns, it becomes possible to directly extract the fields of the record like the code below.

  Double getSum(Object o) {
    if (o instanceof Point(double x, double y)) {
      return x + y;
    }

    return null;
  }

It also supports nested record deconstructing. Below is a more complex example. There is a record named Line which has fields of type ColoredPoint record. The ColoredPoint itself contains a field whose type is Point record.

  enum Color { RED, GREEN }
  record Point(double x, double y) {}
  record ColoredPoint(Point point, Color color) {}
  record Line(ColoredPoint point1, ColoredPoint point2) {}

The example below implements record patterns on Line, ColoredPoint, and Point.

  static void printLine(Object obj) {
    if (obj instanceof Line(
        ColoredPoint(Point(double x1, double y1), Color color1),
        ColoredPoint(Point(double x2, double y2), Color color2)
    )) {
      System.out.printf(
          "%s(%s, %s); %s(%s, %s)%n",
          color1,
          x1,
          y1,
          color2,
          x2,
          y2
      );
    }
  }

Switch Expression Usage

JEP420 added support for switch expression to include patterns including record patterns. That makes it possible to use switch expressions by adding cases where the value matches a record type. It supports deconstruction of the record fields as well. Keep in mind that every switch expression must be exhaustive (cover all possible cases). To understand whether a switch expression is exhaustive, read the examples below.

Below is an example of a non-exhaustive switch expression. Because the value passed to the switch is an Object, it has to cover all types which is a sub-type of Object. However, it only covers the case when the object is a Point.

  private void nonExhaustiveExample1() {
    Object obj = new Point(1, 2);

    // Error
    int result = switch (obj) {
      case Point p -> 1;
    };
    System.out.println(result);
  }

To make it exhaustive, add a default case.

  private void exhaustiveExample1() {
    Object obj = new Point(1, 2);

    double result = switch (obj) {
      case Point p -> p.x + p.y;
      default -> 0;
    };
    System.out.println(result);
  }

Below is another example that uses record deconstruction.

  private void exhaustiveExample1b() {
    Object obj = new Point(1, 2);

    double result = switch (obj) {
      case Point(double x, double y) -> x + y;
      default -> 0;
    };
    System.out.println(result);
  }

For the next examples, we have a record named Pair with a generic parameterized type. With the support of deconstruction, it becomes possible to differentiate the cases based on the parameterized types.

  static class Animal {}
  static class Bird extends Animal {}
  sealed interface Pet permits Cat, Dog {}
  static final class Cat implements Pet {}
  static final class Dog implements Pet {}
  record Pair<T>(T x, T y) {}

The example below is not exhaustive because it doesn't cover the case when both parameters are not a Bird. The reason is the classes that can extend Animal class are not limited.

  private void nonExhaustiveExample2() {
    Pair<Animal> pair = new Pair<>(new Bird(), new Bird());

    // Error
    int result = switch (pair) {
      case Pair<Animal>(Animal animal, Bird bird) -> 1;
      case Pair<Animal>(Bird bird, Animal animal) -> 2;
    };
    System.out.println(result);
  }

The correction for the previous example is by adding another case when both parameters are Animal. You can also add a default case.

  private void exhaustiveExample2() {
    Pair<Animal> pair = new Pair<>(new Bird(), new Bird());

    int result = switch (pair) {
      case Pair<Animal>(Animal animal, Bird bird) -> 1;
      case Pair<Animal>(Bird bird, Animal animal) -> 2;
      case Pair<Animal>(Animal bird, Animal animal) -> 3;
    };
    System.out.println(result);
  }

Another solution is by using a sealed class or interface. By doing so, the switch expression will be exhaustive as long as you cover all cases for the permitted classes.

  private void exhaustiveExample3() {
    Pair<Pet> pair = new Pair<>(new Cat(), new Dog());

    int result = switch (pair) {
      case Pair<Pet>(Pet pet, Cat cat) -> 1;
      case Pair<Pet>(Pet pet, Dog dog) -> 2;
    };
    System.out.println(result);
  }

Summary

In this tutorial, we have learned about the usage of record patterns in Java. When using instanceof, it allows you to extract the fields directly. It also works on switch expressions. You can add cases where the value matches a record. The pattern matching for record also supports generic types and allows field deconstruction. Make sure that each switch expression has covered all possible cases to avoid compile time error.

To use record patterns, you have to use Java version 19 or newer. As long as this feature is still in preview in the Java version you use, you have to enable preview when running the code.

  java --source 19 --enable-preview /path/to/YourClass.java

You can also read about: