Skip to main content
pragmatic clean code minimizing cognitive load in production java

Smart Switches and Pattern Matching

3 min read Chapter 11 of 25
Summary

Java 21's sealed interfaces restrict inheritance to known...

Java 21's sealed interfaces restrict inheritance to known types, enabling compiler exhaustiveness checks. Combined with pattern matching for switch (JEP 441), this eliminates 'else' chains and manual instanceof checks. Record patterns (JEP 440) deconstruct data directly. Guarded patterns ('when') add conditional logic. The approach reduces cognitive load and makes code more maintainable.

Smart Switches and Pattern Matching

Introduction to Sealed Interfaces and Pattern Matching

Java 21 introduces significant enhancements to the language, notably the finalized Pattern Matching for switch as part of JEP 441. This feature, combined with sealed interfaces and records, revolutionizes how developers handle complex logic and data modeling in Java. Sealed interfaces, which specify which classes or interfaces may implement them, restrict the hierarchy to a known, finite set of types. This restriction enables the Java compiler to perform exhaustiveness checks on switch expressions, eliminating the need for a default case if all permitted subclasses are covered.

Benefits of Sealed Interfaces with Pattern Matching

The integration of sealed interfaces with pattern matching for switch expressions brings about several benefits. It reduces cognitive load by bringing logic and data together in a declarative style, making the code more readable and maintainable. The use of sealed interfaces with pattern matching also eliminates the need for ‘else’ chains and manual ‘instanceof’ checks, which can lead to more robust and less error-prone code. Furthermore, switch expressions return a value, whereas switch statements perform an action, providing more flexibility in coding practices.

Example: Exhaustive Switch Expression

Consider the following example that demonstrates an exhaustive switch expression using sealed interfaces and records in Java 21:

sealed interface UserNotification permits Email, SMS, Push {}

record Email(String address, String subject) implements UserNotification {}
record SMS(String phoneNumber, String body) implements UserNotification {}
record Push(String deviceId, String message) implements UserNotification {}

public class Notifier {
    public String getNotificationSummary(UserNotification notice) {
        return switch (notice) {
            case Email(var addr, var sub) -> "Email to " + addr + ": " + sub;
            case SMS(var phone, var body) -> "SMS to " + phone;
            case Push(var dev, var msg)  -> "Push to " + dev;
            // No default required! Compiler knows these are the only 3 types.
        };
    }
}

This example showcases how sealed interfaces (UserNotification) and records (Email, SMS, Push) can be used with pattern matching to handle different types of notifications in an exhaustive manner, without the need for a default case.

Pattern Matching with Guarded Patterns

Java 21 also introduces guarded patterns, which allow for the combination of a pattern with a boolean expression using the ‘when’ keyword. This feature further refines the matching logic within a switch case, enabling more complex and precise handling of data. For instance:

public String handleSensitiveSms(UserNotification notice) {
    return switch (notice) {
        case SMS(var phone, var body) when body.contains("OTP") -> "Secure OTP sent to " + phone;
        case SMS(var phone, var body) -> "Standard SMS sent to " + phone;
        case Email e -> "Email for " + e.address();
        case Push p  -> "Generic Push";
    };
}

In this example, the guarded pattern is used to differentiate between SMS notifications containing OTP (One-Time Password) and standard SMS notifications, demonstrating the power of combining patterns with conditional logic.

Conclusion

The combination of sealed interfaces, pattern matching for switch, and records in Java 21 offers a powerful approach to handling complex logic and data modeling. By leveraging these features, developers can write more concise, readable, and maintainable code, reducing the likelihood of errors and improving overall code quality. As Java continues to evolve, embracing these advancements will be crucial for creating efficient, scalable, and robust applications.

Sources

No external sources were cited in this section.