Pattern Matching Using Switch in Java
Type Patterns
Pattern matching using switch was introduced in Java 17. It is used similar to pattern matching using the instanceof operator but instead using usually a switch expression, although you can use a switch statement. Type patterns are written with the following syntax:
switch (object) {
case String s -> System.out.println("String: " + s);
case Integer i -> System.out.println("Integer: " + i);
default -> System.out.println("default");
}
This will execute from top to bottom until it matches one of the types. Each case has a type pattern and a pattern variable. The scope of the pattern variable is from when it is declared to the end of the block of code for the pattern that was matched. This is the equivalent of the following, but using instanceof instead.
if (object instanceof String s) {
System.out.println("String: " + s);
} else if (object instanceof Integer i) {
System.out.println("Integer: " + i);
} else {
System.out.println("default");
}
Using type patterns, you can match any class or array. You cannot match primitive types unless they are primitive arrays.
Guarded Pattern Label (when Clause)
After a type pattern, you can add a boolean expression by using a when clause. This is called a guarded pattern label. Using just a type pattern, you might have code like the following:
final int value = switch (object) {
case int[] i -> {
if (i.length > 0) {
yield i[0];
} else {
yield 0;
}
}
default -> 0;
};
This can be simplified using the pattern variable in a when clause.
final int value = switch (object) {
case int[] i when i.length > 0 -> yield i[0];
default -> 0;
};
The expression in the when clause can be written just like any other expression using &&, || and parenthesis.
Null case Labels
If you pass null to a switch like what is shown in the following example, it will throw a NullPointerException.
final Object object = null;
// Throws NullPointerException
switch (object) {
case String s -> System.out.println(s);
default -> System.out.println("default");
}
In Java 21, switches were enhanced to allow for a null label. If a switch has a null label and the value passed to it is null, it will instead match the null label and not throw a NullPointerException.
final Object object = null;
switch (object) {
case String s -> System.out.println(s);
case null -> System.out.println("null");
default -> System.out.println("default");
}
You cannot combine null with anything other than default.
switch (object) {
case String s -> System.out.println(s);
case null, default -> System.out.println("null or default");
}
If you try to combine it in anything other than default, it will result in a compiler error.
Compiler Enforcement
When using pattern matching, the compiler will enforce multiple things. The compiler ensures code is more safe and less error-prone to bugs that occur from invalid logic or invalid types. This is something if statements cannot provide. Because of this, pattern matching using a switch is usually the better and preferred way to do pattern matching.
Type Coverage
The Java compiler will enforce that all cases are exhausted when using pattern matching with switches. An enumeration has a known number of values. If any of these are not in the switch, and the switch doesn't contain a default label, it will generate a compiler error. The following shows a switch statement not matching all values in the enumerations.
enum TrafficLightColor {
RED,
YELLOW,
GREEN;
}
// Compiler error:
// the switch expression does not cover all input values
final String result = switch (color) {
case GREEN -> "Green";
case YELLOW -> "Yellow";
}
To fix this compiler error, you'd either have to add a default label or the missing enum RED.
This is the same thing with sealed classes. Sealed classes have a known number of types. If you are matching based on a sealed class, it will follow the same behavior as enumerations shown above.
Pattern Label Dominance
Labels are checked for a match from top to bottom. Since multiple labels could possibly match, the Java compiler will make sure all cases can match. It is possible to order labels in a way that one may never match but ordered in another way, all will match. For example, the following code will generate a compiler error because it contains an unreachable label.
switch (object) {
case Number n -> System.out.println("Number");
// Compiler error:
// this case label is dominated by a preceding case label
case Integer i -> System.out.println("Integer");
default -> System.out.println("default");
}
Since Number is the superclass of Integer, Integer will never match because Number is listed before Integer. An Integer is a Number, but a Number isn't necessarily an Integer. Switching these two eliminates the compiler error since both could match.
switch (object) {
case Integer i -> System.out.println("Integer");
case Number n -> System.out.println("Number");
default -> System.out.println("default");
}
Conclusion
Pattern matching can be used with both switch statements and switch expressions. They are a great way to have additional help from the compiler, something that if statements cannot do. When matching types, use an if statement for a single match, and anything more, use a switch expression instead.