Course – LS – All

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

>> CHECK OUT THE COURSE

1. Overview

The Java SE 17 release introduces pattern matching for switch expressions and statements (JEP 406) as a preview feature. Pattern matching provides us more flexibility when defining conditions for switch cases.

In addition to case labels that can now contain patterns, the selector expression is no longer limited to just a few types. Before pattern matching, switch cases supported only simple testing of a selector expression that needs to match a constant value exactly.

In this tutorial, we will cover three different pattern types that can be applied in switch statements. We’ll also explore some switch specifics, like covering all values, ordering subclasses, and handling null values.

2. Switch Statement

We use switch in Java to transfer control to one of the several predefined case statements. Which statement gets selected depends on the value of the switch selector expression.

In the earlier versions of Java, the selector expression had to be a number, a string, or a constant. Also, the case labels could only contain constants:

final String b = "B";
switch (args[0]) {
    case "A" -> System.out.println("Parameter is A");
    case b -> System.out.println("Parameter is b");
    default -> System.out.println("Parameter is unknown");
};

In our example, if variable b wasn’t final, the compiler would throw a constant expression required error.

3. Pattern Matching

Pattern matching, in general, was first introduced as a preview feature in Java SE 14.

It was limited to only one form of a pattern – the type pattern. A typical pattern consists of a type name and the variable to bind the result to.

Applying type patterns to the instanceof operator simplifies type checking and casting. Moreover, it enables us to combine both into a single expression:

if (o instanceof String s) {
    System.out.printf("Object is a string %s", s);
} else if (o instanceof Number n) {
    System.out.printf("Object is a number %n", n);
}

This built-in language enhancement helps us write less code with enhanced readability.

4. Patterns for Switch

Pattern matching for instanceof became a permanent feature in Java SE 16.

With Java 17, the application of pattern matching now also expands to switch expressions.

However, it is still a preview feature, so we need to enable preview to use it:

java --enable-preview --source 17 PatternMatching.java

4.1. Type Pattern

Let’s look at how type patterns and the instanceof operator can be applied in switch statements.

As an example, we’ll create a method that converts different types to double using if-else statements. Our method will simply return zero if the type is not supported:

static double getDoubleUsingIf(Object o) {
    double result;
    if (o instanceof Integer) {
        result = ((Integer) o).doubleValue();
    } else if (o instanceof Float) {
        result = ((Float) o).doubleValue();
    } else if (o instanceof String) {
        result = Double.parseDouble(((String) o));
    } else {
        result = 0d;
    }
    return result;
}

We can solve the same problem with less code using type patterns in switch:

static double getDoubleUsingSwitch(Object o) {
    return switch (o) {
        case Integer i -> i.doubleValue();
        case Float f -> f.doubleValue();
        case String s -> Double.parseDouble(s);
        default -> 0d;
    };
}

In earlier versions of Java, the selector expression was limited to only a few types. However, with type patterns, the switch selector expression can be of any type.

4.2. Guarded Pattern

Type patterns help us transfer control based on a particular type. However, sometimes, we also need to perform additional checks on the passed value.

For example, we may use an if statement to check the length of a String:

static double getDoubleValueUsingIf(Object o) {
    return switch (o) {
        case String s -> {
            if (s.length() > 0) {
                yield Double.parseDouble(s);
            } else {
                yield 0d;
            }
        }
        default -> 0d;
    };
}

We can solve the same problem using guarded patterns. They use a combination of a pattern and a boolean expression:

static double getDoubleValueUsingGuardedPatterns(Object o) {
    return switch (o) {
        case String s && s.length() > 0 -> Double.parseDouble(s);
        default -> 0d;
    };
}

Guarded patterns enable us to avoid additional if conditions in switch statements. Instead, we can move our conditional logic to the case label.

4.3. Parenthesized Pattern

In addition to having conditional logic in the cases label, parenthesized patterns enable us to group them.

We can simply use parentheses in our boolean expressions when performing additional checks:

static double getDoubleValueUsingParenthesizedPatterns(Object o) {
    return switch (o) {
        case String s && s.length() > 0 && !(s.contains("#") || s.contains("@")) -> Double.parseDouble(s);
        default -> 0d;
    };
}

By using parentheses, we can avoid having additional if-else statements.

5. Switch Specifics

Let’s now look at a couple of specific cases to consider while using pattern matching in switch.

5.1. Covering All Values

When using pattern matching in switch, the Java compiler will check the type coverage.

Let’s consider an example switch condition accepting any object but covering only the String case:

static double getDoubleUsingSwitch(Object o) {
    return switch (o) {
        case String s -> Double.parseDouble(s);
    };
}

Our example will result in the following compilation error:

[ERROR] Failed to execute goal ... on project core-java-17: Compilation failure
[ERROR] /D:/Projects/.../HandlingNullValuesUnitTest.java:[10,16] the switch expression does not cover all possible input values

This is because the switch case labels are required to include the type of the selector expression.

The default case label may also be applied instead of a specific selector type.

5.2. Ordering Subclasses

When using subclasses with pattern matching in switch, the order of the cases matters.

Let’s consider an example where the String case comes after the CharSequence case.

static double getDoubleUsingSwitch(Object o) {
    return switch (o) {
        case CharSequence c -> Double.parseDouble(c.toString());
        case String s -> Double.parseDouble(s);
        default -> 0d;
    };
}

Since String is a subclass of CharSequence, our example will result in the following compilation error:

[ERROR] Failed to execute goal ... on project core-java-17: Compilation failure
[ERROR] /D:/Projects/.../HandlingNullValuesUnitTest.java:[12,18] this case label is dominated by a preceding case label

The reasoning behind this error is that there is no chance that the execution goes to the second case since any string object passed to the method would be handled in the first case itself.

5.3. Handling Null Values

In earlier versions of Java, each passing of a null value to a switch statement would result in a NullPointerException.

However, with type patterns, it is now possible to apply the null check as a separate case label:

static double getDoubleUsingSwitch(Object o) {
    return switch (o) {
        case String s -> Double.parseDouble(s);
        case null -> 0d;
        default -> 0d;
    };
}

If there is no null-specific case label, a pattern label of total type will match null values:

static double getDoubleUsingSwitchTotalType(Object o) {
    return switch (o) {
        case String s -> Double.parseDouble(s);
        case Object ob -> 0d;
    };
}

We should note that a switch expression cannot have both a null case and a total type case.

Such a switch statement will result in the following compilation error:

[ERROR] Failed to execute goal ... on project core-java-17: Compilation failure
[ERROR] /D:/Projects/.../HandlingNullValuesUnitTest.java:[14,13] switch has both a total pattern and a default label

Finally, a switch statement using pattern matching can still throw a NullPointerException.

However, it can do so only when the switch block doesn’t have a null-matching case label.

6. Conclusion

In this article, we explored pattern matching for switch expressions and statements, a preview feature in Java SE 17. We saw that by using patterns in case labels, that selection is determined by pattern matching rather than a simple equality check.

In the examples, we covered three different pattern types that can be applied in switch statements. Finally, we explored a couple of specific cases, including covering all values, ordering subclasses, and handling null values.

As always, the complete source code is available 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.