Java's pattern-matching features arrived incrementally across several JDK releases. Pattern matching for instanceof became permanent in Java 16, pattern matching for switch became permanent in Java 21, and record patterns also became permanent in Java 21. Together, these features let developers test types, bind variables and deconstruct records with substantially less casting and boilerplate.
In a previous tutorial we introduced the basics of pattern matching and its integration with switch expressions. Now, we'll dive deeper into more advanced Java pattern-matching techniques and applications: dealing with null values in switch expressions, deconstructing nested records, and streamlining code with type inference, variables and generics.
Java pattern matching version guide
| Feature | Permanent since |
|---|---|
Pattern matching for instanceof |
Java 16 |
Pattern matching for switch |
Java 21 |
| Record patterns | Java 21 |
| Unnamed variables and patterns | Java 22 |
Handling null values in switch expressions
Traditionally, switch statements and expressions throw a NullPointerException if the selector expression evaluates to null. In the past, developers could handle this case separately, such as follows:
public String processGreeting(String greeting) {
if (greeting == null) {
return "The value is null!";
}
return switch (greeting) {
case "hello" -> "You said hello!";
case "goodbye" -> "See you later!";
default -> "Unknown greeting: " + greeting;
};
}Modern pattern-matching switch supports an explicit case null. This capability appeared while pattern matching for switch was being previewed and became a permanent language feature with Java 21. On Java 21 or newer, the null case can be written directly:
public String processGreeting(String greeting) {
return switch (greeting) {
case null -> "The greeting is null!"; // Directly handle null values
case "hello" -> "You said hello!";
case "goodbye" -> "See you later!";
default -> "Unknown greeting: " + greeting;
};
}Guarded patterns
Pattern labels can also include a when guard. A guard adds a boolean condition that must be true after the pattern itself matches, which makes it possible to express more specific cases without moving the condition into the case body.
Consider the following example:
switch (shape) {
case Triangle t -> System.out.println(t + " is a Triangle");
case Rectangle r when r.height() == r.width() -> System.out.println(r + " is a Square");
case Rectangle r -> System.out.println(r + " is a Rectangle");
default -> System.out.println("Unknown shape");
}In this example, the first case matches any Triangle. The second case also matches a Rectangle (r), but the when clause adds an extra requirement of r.height() == r.width(). Therefore, this case is selected only if the rectangle is actually a square. Lastly, the third case acts as a catchall for rectangles that don't fulfill the square condition.
The when clause enables us to create guarded patterns, ensuring that a pattern match must also satisfy an additional condition.
Pattern matching with nested records
Record patterns can be nested, allowing Java to deconstruct several levels of a data structure in one pattern. Consider a Book record that contains an Author record:
record Book(String name, Author author) {}
record Author(String name, String email) {}One can use a switch expression to extract information from a nested record structure. Here's how:
Object book = findBook(); // Assume findBook() returns a Book object
String description = switch (book) {
case Book(String title, Author(String name, String email)) ->
"Title: " + title + ", Author: " + name + ", Email: " + email;
default -> "Book information unavailable.";
};
System.out.println(description);This switch expression matches and deconstructs both records in a single case, exposing the title, author name and email directly to the case expression. The example uses explicit component types; the next example shows how var can let the compiler infer them.
Pattern matching with type inference
In the above example, we have used nested record deconstruction. However, we specified the type in each case. This is not necessarily needed; the Java compiler can automatically infer the type. Let's use the var keyword instead of the explicit type. Consider the following code snippet:
String description = switch (book) {
case Book(var title, Author(var name, var email)) ->
"Title: " + title + ", Author: " + name + ", Email: " + email;
default -> "Book information unavailable.";
};In this improved code, we've replaced explicit type declarations with var.
Unnamed patterns and variables
Unnamed variables and patterns use the single underscore, _, when a value is intentionally not needed. They were previewed in Java 21 and became permanent in Java 22. This is particularly useful with record patterns because unwanted components can be ignored explicitly.
Consider this modified example:
var description = switch (book) {
case Book(_, Author(var name, _)) -> "Author: " + name;
default -> "Book information unavailable.";
};
System.out.println(description);This code gives us more clarity as we focus on what matters only; the rest we can just omit.
Pattern matching and generics
JEP 440 finalized record patterns in Java 21 and supports inference of type arguments for generic record patterns. Consider this generic Box record:
record Box<T>(T t) { }
static void test(Box<String> box) {
if (box instanceof Box(var s)) { // Box<String> is inferred
// Do stuff with s
}
}Modern Java pattern matching can eliminate many explicit casts, temporary variables and nested conditionals. Pattern matching for switch and record patterns are permanent as of Java 21, while unnamed variables and patterns are permanent as of Java 22. If an application targets an older JDK, check whether the feature was unavailable or still required preview flags in that release.
The strongest benefit is not simply shorter syntax. Patterns let the structure being tested, the values being extracted and any additional guard condition appear together, which can make branching logic easier to read and harder to misuse.
In the next article, we'll delve into the intricate relationship between record patterns along with sealed classes and exhaustive switch expressions.
A N M Bazlur Rahman is a Java Champion and staff software developer at DNAstack. He is also founder and moderator of the Java User Group in Bangladesh.