Java 21 Pattern Matching for Switch: Real-World Examples
Java 21 Pattern Matching for Switch: Real-World Examples
Java 21 has finally brought pattern matching for switch expressions and statements out of preview and into the language as a permanent feature (JEP 441). This is a game-changer for how we write conditional logic in Java. If you’ve been using if-else chains or switch statements with type checks and casts, you’re in for a treat. In this post, we’ll explore real-world examples that demonstrate the power and elegance of this feature.
What Is Pattern Matching for Switch?
At its core, pattern matching for switch allows you to test a value against multiple patterns, each of which can include a type check, a deconstruction, or even a guard condition. The compiler handles the type checks and casts for you, making your code safer and more concise.
Consider the old way of handling different types in a switch:
1 | // Old way (pre-Java 21) |
With Java 21, you can write this instead:
1 | // Java 21 pattern matching switch |
The syntax is cleaner, and the compiler ensures that you don’t miss any casts. But this is just the beginning.
Real-World Example 1: Parsing Different Message Types
Imagine you’re building a messaging system that receives payloads of different types: text, image, video, or a command. Without pattern matching, you’d have a chain of if-else or a cumbersome switch on a discriminator field. With pattern matching, you can directly switch on the payload object.
1 | public record TextMessage(String content) {} |
Notice how the case null is explicitly handled. In previous Java versions, a null value would throw a NullPointerException in a switch. Now, you can handle null directly. This eliminates entire categories of bugs.
Real-World Example 2: Processing Shapes in a Graphics Engine
Suppose you have a hierarchy of shapes in a graphics engine. You want to calculate the area and perimeter of each shape. With pattern matching, you can write a single method that handles all shapes elegantly.
1 | sealed interface Shape permits Circle, Rectangle, Triangle {} |
Because Shape is a sealed interface, the compiler knows all possible subtypes. This means you don’t need a default case—if you add a new shape later and forget to update the switch, the compiler will warn you. This is a huge safety net for maintainable code.
Real-World Example 3: Guards for Conditional Logic
Sometimes you need to apply additional conditions to a pattern. For example, you might want to handle a string differently if it starts with a special prefix. Java 21 introduces guards (previously called “when clauses” in preview) that let you refine a pattern.
1 | public void processString(Object obj) { |
Guards can also be used with record patterns. For example, in a trading system, you might want to handle orders above a certain amount differently:
1 | public record Order(String symbol, int quantity, double price) {} |
Real-World Example 4: Nested Pattern Matching with Records
One of the most powerful aspects of Java 21’s pattern matching is the ability to deconstruct nested records. Imagine you have a JSON-like structure represented as records:
1 | public record JsonObject(Map<String, Object> fields) {} |
Notice the use of _ in case JsonNull _. This is a pattern variable that we don’t use, so we can ignore it with an underscore. This is a small quality-of-life improvement that reduces clutter.
Real-World Example 5: Simplifying Visitor Pattern
The Visitor pattern is notorious for its boilerplate. With pattern matching, you can often eliminate the need for a separate visitor class altogether.
Consider an expression evaluator for a simple arithmetic language:
1 | sealed interface Expr permits Constant, Add, Multiply, Negate {} |
This is much cleaner than the traditional visitor pattern, which would require separate visitor interfaces and accept methods for each expression type.
Performance Considerations
Pattern matching for switch is not just syntactic sugar. The Java compiler generates efficient code that is often faster than a chain of instanceof checks. The JVM also applies optimizations such as type profiling and inlining. In most cases, you should prefer pattern matching for its readability and safety, without worrying about performance.
However, be mindful of very large switch expressions with many patterns. The compiler may need to generate a decision tree, which could increase class file size. In practice, this is rarely an issue.
Migration Tips
If you’re upgrading to Java 21 and want to start using pattern matching in your existing codebase, here are some tips:
- Start with simple type checks. Replace
if (x instanceof Foo f) { ... } else if (x instanceof Bar b) { ... }with a switch. - Add null handling. Explicitly handle
nullcases to avoid surprises. - Use sealed classes. If you control the type hierarchy, make it sealed to get exhaustiveness checks from the compiler.
- Refactor gradually. You don’t have to rewrite everything at once. Pattern matching is additive—you can use it alongside legacy code.
Common Pitfalls to Avoid
- Forgetting the default case. Even with sealed types, if your switch is a statement (not an expression), you still need a default unless you cover all possibilities.
- Overusing guards. While guards are powerful, complex boolean expressions can make code hard to read. Consider extracting the guard condition into a method.
- Mixing patterns and constants. You can combine pattern cases with traditional constant cases, but be careful about ordering. The first matching case wins.
Conclusion
Java 21’s pattern matching for switch is one of the most significant language enhancements in years. It makes code more expressive, safer, and easier to maintain. By embracing this feature, you can reduce boilerplate, eliminate common bugs like missing null checks, and write code that clearly communicates your intent.
Key Takeaways
- Pattern matching for switch allows type checks, deconstruction, and guards in a single construct, reducing boilerplate and improving readability.
- Null handling is now explicit with
case null, eliminating a major source of runtime errors. - Sealed types combined with pattern matching give you compile-time exhaustiveness checks, ensuring you handle all cases.
- Record patterns enable deep deconstruction of nested data structures, making code like JSON processing much cleaner.
- Guards (with
&&) let you add additional conditions to patterns, replacing complexif-elsechains. - Performance is on par with or better than traditional
instanceofchains, and the JVM continues to optimize. - Migration can be incremental—start by replacing simple
if-elsechains and gradually adopt more advanced patterns.