Since Java 21, several features regarding switch functionality are available. Pattern matching expands our capabilities when working with switch. In this topic, we will explore this enhancement and practice to help you master it.
Pattern matching semantics
The pattern matching implementation for switch builds on the instanceof operator enhancements. It enables us to perform runtime type checking and execute required operations, such as printing a message. Let's examine the code written using if blocks. Then, we'll rewrite the same code using the new switch feature.
public class SwitchPatternMatchingDemo {
public static void main(String[] args) {
Object obj = "Java";
ifTypeCheckingDemo(obj); // String: Java
}
static void ifTypeCheckingDemo(Object o) {
if (o instanceof Integer i) {
System.out.printf("int: %d", i);
} else if (o instanceof Double d) {
System.out.printf("double: %f", d);
} else if (o instanceof String s) {
System.out.printf("String: %s", s);
} else {
System.out.printf("No Match!");
}
}
}In this example, we pass a variable to the ifTypeCheckingDemo(Object o) method, which checks if it matches any of the three types and prints a corresponding message. In our case, the message is String: Java. Now, let's look at the same code written using switch pattern matching:
public class SwitchPatternMatchingDemo {
public static void main(String[] args) {
Object obj = "Java";
switchTypeCheckingDemo(obj); // String: Java
}
static void switchTypeCheckingDemo(Object o) {
switch (o) {
case Integer i -> System.out.printf("int: %d", i);
case Double d -> System.out.printf("double: %f", d);
case String s -> System.out.printf("String: %s", s);
default -> System.out.println("No Match!");
}
}
}This code is much more concise and readable, making it a compelling reason to use this feature.
Selector expression type checking enhancement
The example in the previous section uses basic Java types. However, what if we need to perform type checking with a non-basic type? Before this update, switch only supported primitive types and their wrapper classes, as well as String and Enum. This enhancement now allows us to use any type for pattern matching.
public class SwitchPatternMatchingDemo {
public static void main(String[] args) {
Object obj = new Person("James Gosling");
switchTypeCheckingDemo(obj); // Person: Person{name=James Gosling}
}
static void switchTypeCheckingDemo(Object o) {
switch (o) {
case Integer i -> System.out.printf("int: %d", i);
case Person p -> System.out.printf("Person: %s", p.toString());
case String s -> System.out.printf("String: %s", s);
default -> System.out.println("No Match!");
}
}
}
class Person {
private String name;
// getter and setter
public Person(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"name=" + name +
'}';
}
}This application will compile and print Person: Person{name=James Gosling}, which wasn't possible before the update.
Pattern matching via logical operators
Pattern matching for switch, like in the case of the instanceof operator, isn't limited to single type checking. You can also use the && logical operator and add an extra statement level:
public class SwitchPatternMatchingDemo {
public static void main(String[] args) {
Object obj = 100.0;
switchTypeCheckingDemo(obj); // double: 100,000000, the number is positive
}
static void switchTypeCheckingDemo(Object o) {
switch (o) {
case Integer i -> {
if (i > 0) {
System.out.printf("int: %d, the number is positive", i);
}
}
case Double d && d > 0 -> System.out.printf("double: %f, the number is positive", d);
default -> System.out.println("No Match!");
}
}
}The switchTypeCheckingDemo(Object o) method contains two case labels. The first one uses a pattern to check the type, with the second statement executing in the if block. The second case performs the same function but uses a pattern for the second statement to verify if the variable value is a positive number.
Note that switch pattern matching, like the instanceof operator, doesn't support the || operator because of its semantics.
Null operations
In earlier Java versions, switch couldn't process null values. The following code will run but throw a NullPointerException when the application executes the first line of the nullDemo(String s) method.
public class SwitchPatternMatchingDemo {
public static void main(String[] args) {
String str = null;
nullDemo(str);
}
static void nullDemo(String s) {
switch (s) {
case "Hello" -> System.out.println("null");
case "Hi" -> System.out.println("String");
default -> System.out.println("No Match!");
}
}
}However, if str is not null and we add a null case, the code won't compile. We will get a compilation error:
public class SwitchPatternMatchingDemo {
public static void main(String[] args) {
String str = "Hello";
nullDemo(str);
}
static void nullDemo(String s) {
switch (s) {
case null -> System.out.println("null"); // Compilation error
case "Hello" -> System.out.println("Hello");
case "Hi" -> System.out.println("Hi");
default -> System.out.println("No Match!");
}
}
}The new features help us avoid these limitations. We can now use a null case label: the example below will compile successfully. We will only encounter a NullPointerException if the selector expression is null and we don't include a null case.
public class SwitchPatternMatchingDemo {
public static void main(String[] args) {
String str = null;
nullDemo(str); // null case
}
static void nullDemo(String s) {
switch (s) {
case null -> System.out.println("null case");
case "Hello" -> System.out.println("Hello");
case "Hi" -> System.out.println("Hi");
default -> System.out.println("No Match!");
}
}
}What will happen when we run this application? The code will compile and print the null case message!
Case refinement or Guarded Pattern
Java 21 also introduces guarded patterns in switch statements. When you want to match a pattern only if a specific condition is true, you can use the when clause. This clause helps you refine a pattern further:
switch (obj) {
case String s
when s.length() > 5 -> {
System.out.println("Long string: " + s);
}
case String s -> {
System.out.println("Short string: " + s);
}
default -> {
System.out.println("Not a string");
}
}If obj is a string and its length is greater than 5, the first case will match. If not, it moves to the second case. We call this a guarded pattern because a condition guards the pattern. Guarded patterns allow you to control case conditions more precisely while keeping your code readable.
Switches and enum constants
Pattern matching in switches now handles enums more effectively. Consider a sealed interface that allows an enum and another class:
sealed interface CardClassification permits Suit, Tarot {}
public enum Suit implements CardClassification { CLUBS, DIAMONDS, HEARTS, SPADES }
final class Tarot implements CardClassification {}You can use enum constants directly as case constants to avoid unnecessary comparisons, which makes your code clearer to read.
static void exhaustiveSwitchWithBetterEnumSupport(CardClassification c) {
switch (c) {
case Suit.CLUBS -> {
System.out.println("It's clubs");
}
case Suit.DIAMONDS -> {
System.out.println("It's diamonds");
}
case Suit.HEARTS -> {
System.out.println("It's hearts");
}
case Suit.SPADES -> {
System.out.println("It's spades");
}
case Tarot t -> {
System.out.println("It's a tarot");
}
}Additionally, you can combine this with pattern matching for more sophisticated logic in enum handling.
Dominance of case labels
To prevent unreachable code and ambiguity, Java enforces dominance rules in switch pattern matching. A more specific pattern can't follow a broader one that would match first.
For example:
switch (obj) {
case CharSequence cs -> System.out.println("A CharSequence");
case String s -> System.out.println("A string"); // ERROR: already matched by above
}Because every String is a CharSequence, the second case will never run. Java will stop you and display an error.
Instead, you should arrange patterns from more specific to more general:
switch (obj) {
case String s -> System.out.println("A string");
case CharSequence cs -> System.out.println("A CharSequence");
}Exhaustiveness and Sealed Classes
As you know, it's important that a switch covers all possible values, which is why we use default. Java 21 enhances this checking—also known as exhaustiveness checking—particularly when using sealed classes. The compiler now makes sure all possible subtypes are handled, making the code safer and more predictable.
sealed interface Shape permits Circle, Rectangle {}
final class Circle implements Shape {}
final class Rectangle implements Shape {}
static void describe(Shape shape) {
switch (shape) {
case Circle c -> System.out.println("It's a circle");
case Rectangle r -> System.out.println("It's a rectangle");
// No default needed; all subtypes are covered
}
}In this example, you don't need to implement a default case because all subtypes are covered. However, if someone adds a new subtype in the future, Java will require you to update this switch.
Conclusion
In this topic, we explored an important update to the switch feature: pattern matching. This language feature will help you write more concise and expressive code.