Looking at Java 21: Record Patterns
Pattern matching is a declarative and composable approach that creates more powerful and expressive code for data structure navigation and processing.
Java 16 added pattern matching for the instanceof
operator (JEP 394), and we looked at pattern matching for switch
in this series before (Switch Pattern Matching).
Today, it’s time to take a quick look at another kind of pattern matching: Record Patterns (JEP 440).
Table of Contents
More Than Just Type Matching
So far, pattern matching in Java is mainly restricted to matching types:
Java 21 extended the concept to be usable in switch
statements and expressions:
As you can see, the resulting code looks nicer and is more streamlined than before. But matching types is only one possible use case for pattern matching.
Deconstructing Records
Records are a special purpose class to easily aggregate data in a shallowly immutable fashion.
They’re structured around components, similar to fields in a POJO or JavaBean.
Their accessors, the “all components” (canonical) constructor, and Object
-related helper method (toString
, equals
, hashCode
) are all available with sensible implementations without requiring any additional code.
If you want to learn more about Records, you could check out my book “A Functional Approach to Java”, which discusses the topic on over 30 pages.
Record pattern matching is a way to match the Record’s type and access its components in a single step. Imaging a simple Record representing a 2-dimensional point:
Matching its type and accessing one of its components looks like this:
To not match the Point but its components, they must be explicitly stated in the pattern:
If you’re like me when I first looked at the feature, you might think: “ok, but how is that supposed to be better/simpler than before?”
Well, in a certain sense, this way of thinking is correct. Repeating the Record’s definition to access its components seems tedious. But if we look further than such a simple example, the potential of what Record pattern matching can do for us will reveal itself!
Nested Records
Deconstructing a simple Record doesn’t have much of an advantage, in my opinion, at least without a feature I’m going to discuss shortly. The real power of deconstructing Records is found if a Record contains another Record.
Let’s design a Record representing a window frame, including its origin and size on the screen:
To access the height
component of a WindowFrame
in the nested Size
component, we’d need multiple matches:
It doesn’t become that much better with deconstruction:
However, the deconstruction of Records can be nested, eliminating the need for the null
check and making the code more reasonable in the process:
The difference here is that a simple WindowFrame(Point origin, Size size)
matches even if Size size
is null
.
When you deconstruct Size
, too, though, it only matches if size
isn’t null
.
In essence, either the full pattern matches or none of it.
Simpler Patterns with Type Inference
Requiring the full Records declaration for destructuring feels like a chore. The components must match, or the compiler won’t be happy:
As the required components are fixed by the Record’s type, we can use local variable type inference by replacing the actual component types with the var
keyword:
This also won’t break the code if a Record’s component type changes. On the other hand, an explicit type declaration might be more expressive, and I’m sure that IDEs will help out completing the components in a future release.
Even Simpler Patterns with JEP 443
Another upcoming feature only available as a preview in Java 21 is JEP 443: Unnamed Patterns and Variables.
This feature improves readability throughout our code by allowing us to replace an unused variable with _
(underscore).
No more @SuppressWarnings("unused")
needed to shut up all those pesky warnings!
Nameless variables are quite useful in many scenarios, like the Exception variable in a catch
block or side-effect-only constructs:
Regarding Record pattern matching, this JEP simplifies (nested) calls as much as possible:
Now, the pattern is reduced to just what’s needed to match, with less surrounding noise.
Conclusion
Pattern matching is a feature that was a long time absent in Java, or only available in a minuscule form compared to other languages. Adding Record destructuring is another great addition to narrow the feature gap and improve Java’s foundation further.
Deconstructing Record-based data structures using a more straightforward approach to navigate them leads to more reasonable and cleaner code, especially with nested Records. However, to be honest, at first, I didn’t see much of an advantage of Record pattern matching, especially compared to the impact of other features in Java 21.
As I tried to understand the benefits better (by writing an article about it), I didn’t like it much, especially the kind of repetitive syntax. But the more I played around with it, it became clearer that this won’t be an “everyday” feature, like Records themselves, at least for me.
Still, I believe it’s an interesting and worthwhile addition to the language, so give it a try (and some time), you might like it! And with upcoming features like unnamed patterns (JEP 443), Record pattern matching becomes even better!
Resources
- JEP 440: Record Patterns
- JEP 394: Pattern Matching for instanceof
- JEP 443: Unnamed Patterns and Variables (Preview)
Looking at Java 21
- Intro
- String Templates (JEP 430)
- Simpler Main Methods and Unnamed Classes (JEP 445)
- Sequenced Collections (JEP 431)
- Scoped Values (JEP 446)
- Switch Pattern Matching (JEP 441)
- Feature Deprecations (JEP 449, 451)
- Record Pattern Matching (JEP 440)
- Generational ZGC (JEP 439)
- Virtual Threads (JEP 444)
- The Little Things