Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we’ll discuss the new preview feature JEP-405: record patterns in Java SE 19. We’ll see how to decompose record values and how to combine record patterns with type patterns.

2. Model

We’ll use these both records a GPSPoint which has a latitude and a longitude:

public record GPSPoint (double latitude, double longitude) {}

and a Location that contains a name and a GPSPoint:

public record Location (String name, GPSPoint gpsPoint) {}

3. Overview

A record pattern is a construct that allows us to match values against a record type and bind variables to the corresponding components of the record. We can also give the record pattern an optional identifier, which makes it a named record pattern and allows us to refer to the record pattern variable.

3.1. instanceof

Pattern Matching for instanceof, which was introduced in Java 16, allows us to declare a variable directly during the instanceof check. Since Java 19 it now also works with records:

if (o instanceof Location location) {
    System.out.println(loocation.name());
}

We can also extract the values of the pattern variable location into variables using pattern matching. We can omit the pattern variable location because calling the accessor method name() becomes unnecessary:

if (o instanceof Location (String name, GPSPoint gpsPoint)) {
    System.out.println(name);
}

If we have an object o that we want to match against the record pattern. The pattern will only match if it is an instance of the corresponding record type. If the pattern matches, it initializes the variables and casts them to the corresponding type. Keep in mind: the null value doesn’t match any record pattern. We can replace the type of the variables with var. In that specific case, the compiler will infer the type for us:

if (o instanceof Location (var name, var gpsPoint)) { 
    System.out.println(name); 
}

We can even get a step further and destruct the GPSPoint as well:

if (o instanceof Location (var name, GPSPoint(var latitude, var longitude))) {
    System.out.println("lat: " + latitude + ", lng: " + longitude);
}

This is called nested destruction. It helps us with data navigation and allows us to directly access the latitude and longitude without using the getters of Location to get the GPSPoint and then use the getter on the GPSPoint object to get the latitude and longitude values.

We can also use this for generic records. Let’s introduce a new generic record called Wrapper:

public record Wrapper<T>(T t, String description) { }

This record wraps an object of any type and allows us to add a description to it. We can still use instanceof as previous and even destruct the record:

Wrapper<Location> wrapper = new Wrapper<>(new Location("Home", new GPSPoint(1.0, 2.0)), "Description");
if (wrapper instanceof Wrapper<Location>(var location, var description)) {
    System.out.println(description);
}

The compiler can also infer the type of the variable location.

3.2. Switch Expression

We can also use switch expressions to do specific actions based on our object types:

String result = switch (o) {
    case Location l -> l.name();
    default -> "default";
};

It will look for the first matching case. And again, we can also use nested destruction:

Double result = switch (object) {
    case Location(var name, GPSPoint(var latitude, var longitude)) -> latitude;
    default -> 0.0;
};

We have to remember that we always put in a default case for the case when there is no match.

If we want to avoid the default case, we could also use a sealed interface and permit the objects which have to implement the interface:

public sealed interface ILocation permits Location {
    default String getName() {
        return switch (this) {
            case Location(var name, var ignored) -> name;
        };
    }
}

This helps us eliminate the default case and only create the corresponding cases.

It is also possible to guard specific cases. For example, we’d use the when keyword to check for equality and introduce a new Location if there is some unwanted behavior:

String result = switch (object) {
    case Location(var name, var ignored) when name.equals("Home") -> new Location("Test", new GPSPoint(1.0, 2.0)).getName();
    case Location(var name, var ignored) -> name;
    default -> "default";
};

If this switch expression is called by the following object:

Object object = new Location("Home", new GPSPoint(1.0, 2.0));

It would assign the variable result to “Test”. Our object is of the Location record type, and its name is “Home”. Therefore it jumps directly to the first case of the switch. If the name wasn’t “Home”, it would jump into the second case. If the object isn’t of type Location at all, the default value would have been returned instead.

4. Conclusion

In this article, we saw how record patterns allow us to extract the values of a record into variables using pattern matching. We can use instanceof, switch statements, and even guards with additional conditions to do this. Record patterns are especially useful when working with nested records or hierarchies of sealed records.

As always, these examples can also be found over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.