Pattern matching for switch
statements and expressions was introduced in JDK 17 and refined in the following releases. This feature evolved together with record patterns included into JDK 19. Both features were finalized in Java 21, so let’s see how we can use them in development.
The article contains some theory on switch
statements / expressions and pattern matching, so if you are already familiar with the concepts, feel free to jump to the practical section.
Table of Contents
What are switch statements and expressions
The switch
statements are statements with multiple execution paths. Using switch
instead of traditional if-else statements helps to significantly reduce boilerplate code. So, for instance, instead of this code:
String dayOfWeek = "Tuesday";
String moodOfTheDay;
if (dayOfWeek.equals("Monday")) {
moodOfTheDay = "Ready to conquer the world";
} else if (dayOfWeek.equals("Tuesday")) {
moodOfTheDay = "In desperate need of a chocolate cake";
} else if (dayOfWeek.equals("Wednesday")) {
moodOfTheDay = "Exhausted";
} else if (dayOfWeek.equals("Thursday")) {
moodOfTheDay = "Exhausted";
} else if (dayOfWeek.equals("Friday")) {
moodOfTheDay = "Party hard";
} else if (dayOfWeek.equals("Saturday")) {
moodOfTheDay = "Where am I?";
} else if (dayOfWeek.equals("Sunday")) {
moodOfTheDay = "Watching the clouds pass by";
} else {
moodOfTheDay = "There's no such day";
}
System.out.println(moodOfTheDay);
We can write this:
String dayOfWeek = "Tuesday";
String moodOfTheDay;
switch (dayOfWeek) {
case "Monday": moodOfTheDay = "Ready to conquer the world";
break;
case "Tuesday": moodOfTheDay = "In desperate need of a chocolate cake";
break;
case "Wednesday": moodOfTheDay = "Exhausted";
break;
case "Thursday": moodOfTheDay = "Exhausted";
break;
case "Friday": moodOfTheDay = "Party hard";
break;
case "Saturday": moodOfTheDay = "Where am I?";
break;
case "Sunday": moodOfTheDay = "Watching the clouds pass by";
break;
default: moodOfTheDay = "There's no such day";
break;
}
System.out.println(moodOfTheDay);
A switch
statement consists of a body in braces (switch
block), which includes one or multiple labels followed by statements (code to be executed): the case
labels containing values to which the switch
expression is compared, and an optional default
label, whose statement is executed in case the switch
expression doesn’t match any value in the case labels.
The example above is the traditional fall-through switch
statement with a case …:
label. Fall-through means that all switch
statements will be executed unless there’s a break
clause in each. This semantics has long been an irritation point for developers as it is verbose and error-prone. So, Java 14 brought a new case … ->
label that
1. Means that only one statement after the label matching the expression will be executed even without the break
clause, and
2. Allows for multiple constants per case
.
Therefore, the code above can be simplified to:
String dayOfWeek = "Tuesday";
String moodOfTheDay;
switch (dayOfWeek) {
case "Monday" -> moodOfTheDay = "Ready to conquer the world";
case "Tuesday" -> moodOfTheDay = "In desperate need of a chocolate cake";
case "Wednesday", "Thursday" -> moodOfTheDay = "Exhausted";
case "Friday" -> moodOfTheDay = "Party hard";
case "Saturday" -> moodOfTheDay = "Where am I?";
case "Sunday" -> moodOfTheDay = "Watching the clouds pass by";
default -> moodOfTheDay = "There's no such day";
}
System.out.println(moodOfTheDay);
Java 14 also introduced switch expressions that assigns a value to the given variable directly. So, we can simplify our example even more:
String dayOfWeek = "Tuesday";
String moodOfTheDay = switch (dayOfWeek) {
case "Monday" -> "Ready to conquer the world";
case "Tuesday" -> "In desperate need of a chocolate cake";
case "Wednesday", "Thursday" -> "Exhausted";
case "Friday" -> "Party hard";
case "Saturday" -> "Where am I?";
case "Sunday" -> "Watching the clouds pass by";
default -> "There's no such day";
};
System.out.println(moodOfTheDay);
But despite the fact that switch
expressions became more developer-friendly, they still possessed some limitations:
- Support for a limited number of types: primitive types (int, byte, char, short, except for long), their boxed forms (Integer, Byte, Short, Character), enums, and String;
- Special treatment of null values: separate code block outside of a
switch
statement / expression for handling nulls is required; - The
switch
expression can be tested against constants incase
labels only for exact equality.
This is where pattern matching comes to rescue.
What is pattern matching
Pattern matching means testing an object against a particular structure and then extracting values from the object if it matches some structure. A pattern consists of a test applied to the target (the object that we check) and a set of pattern variables, which are extracted from the target only if the test passes successfully.
Pattern matching has already been used in regular expressions. But this feature was extended to the instanceof
operator in JEP 394 for Java 16. Thanks to pattern matching for instanceof
, instead of introducing a local variable, assigning the given expression, casting it to specific type, and only then processing the expression like this:
Object myObject = "This is a String";
if (myObject instanceof String) {
String s = (String) myObject;
System.out.println(s.toUpperCase());
}
We can simply write this:
Object myObject = "This is a String";
if (myObject instanceof String s) {
System.out.println(s.toUpperCase());
}
Where String s
is a type pattern, which is a pattern that takes type as a test and contains one pattern variable, to which the target is assigned.
In further JDK releases, the scope of pattern matching was extended to switch
statements / expressions to overcome their limitations and avoid boilerplate with multiple if-else statements.
How to use pattern matching for switch
Prerequisites:
- JDK 21. You can use Liberica JDK 21 for your platform by getting it directly from the website or using your favorite package manager.
- IDE of choice.
Patterns in case labels
The syntax of a switch
statement / expression doesn’t change, only now we use type patterns in case labels, plus we can use any type to test against as opposed to the traditional switch
:
static String patternTypes(Object myObject) {
return switch (myObject) {
case Integer i -> "This is an Integer: " + i;
case Long l -> "This is a Long: " + l;
case Dog d -> "This is a dog that makes " + d.voice();
case ArrayList a -> "This is an ArrayList of size " + a.size();
default -> "This is some other object: " + myObject.getClass();
};
}
public static class Dog {
String voice = "Woof!";
public String voice() {
return voice;
}
}
Pattern matching with the null case label
What about the null values? How can we handle them? There’s no need to carry the logic of testing against null outside the switch
block, we can now do that inside the block using a case null
label:
static String patternTypes(Object myObject) {
return switch (myObject) {
case Integer i -> "This is an Integer: " + i;
case Long l -> "This is a Long: " + l;
case Dog d -> "This is a dog that makes " + d.voice();
case ArrayList a -> "This is an ArrayList of size " + a.size();
case null -> "You gave me a null value";
default -> "This is some other object: " + myObject.getClass();
};
}
Note that if you don’t add the null case, switch
will throw a NullPointerException
like it always has.
Guarded pattern case labels and a when clause
What if we need to further test the value when it matches some pattern? For instance, we want to accept only Integers of certain values. One way is to use nested if-else statements, but it leads to redundant and potentially error-prone code.
Luckily, we can use guarded pattern case
labels that contain a type pattern and a boolean expression or guard.
static String guardedPatterns (Object myObject) {
return switch (myObject) {
case Integer i when i == 1 -> "Connecting you to the Manager...";
case Integer i when i == 2 -> "Connecting you to the Sales Department...";
default -> "Invalid input";
};
}
How to order case labels
Let’s linger on the previous example for a while. We can add a condition that checks whether the given object is an Integer of any value rather than 1 or 2:
static String guardedPatterns (Object myObject) {
return switch (myObject) {
case Integer i when i == 1 -> "Connecting you to the Manager...";
case Integer i when i == 2 -> "Connecting you to the Sales Department...";
case Integer i -> "Invalid number";
default -> "Totally invalid input!";
};
}
The code functions correctly, but what if we change the case order and put case Integer i ->
on top?
static String guardedPatterns (Object myObject) {
return switch (myObject) {
case Integer i -> "Invalid number";
case Integer i when i == 1 -> "Connecting you to the Manager...";
case Integer i when i == 2 -> "Connecting you to the Sales Department...";
default -> "Totally invalid input!";
};
}
In this case, you will get a compile-time error java: this case label is dominated by a preceding case label
.
What went wrong?
If we place case Integer i ->
before case Integer i when i == 1
, the first pattern case
will dominate the second pattern case
, meaning that any Integer matches the more general condition, and so the switch
won’t go any further to analyze more specific cases and execute this statement. Therefore,
- A more general (unguarded) pattern
case
label dominates the guardedcase
label with the same condition; - A pattern
case
label dominates the constantcase
label, - A type of a pattern
case
label dominates the subtype of a patterncase
label.
So, for instance, placing a parent class in a pattern case
over a child class like that
static String patternOrder(Object myObject) {
return switch (myObject) {
case Animal a -> "This is an animal that makes " + a.voice();
case Dog d -> "This is a dog that makes " + d.voice();
default -> "This is not an animal";
};
}
public static class Animal {
String voice = "a random sound";
public String voice() {
return voice;
}
}
public static class Dog extends Animal {
String voice = "woof!";
public String voice() {
return voice;
}
}
Will also result in a compile-time error.
To conclude this section, there’s a simple rule to ordering pattern case
labels: move down from more specific to more general conditions. Constant case
labels should come first, then guarded pattern case
labels, then unguarded pattern case
labels. Subtypes should precede types.
Exhaustiveness of switch statements and expressions
Exhaustiveness of switch
statements / expressions implies that all possible values of the selector expression must be handled in the switch
block.
Let’s take one of the snippets we created above. If we remove the default
case like that
static String patternTypes(Object myObject) {
return switch (myObject) {
case Integer i -> "This is an Integer: " + i;
case Dog d -> "This is a dog that makes " + d.voice();
case ArrayList a -> "This is an ArrayList of size " + a.size();
};
}
it will result in compile-time error java: the switch expression does not cover all possible input values
.
This functionality is especially useful with enums and sealed classes. Consider the following enum class:
enum DeliveryStatus { IN_TRANSIT, DELIVERED, CANCELED }
If we want to use a switch
statement / expression with our enum, we can specify all possible input values without the default
clause, and leave exhaustiveness checks to the compiler.
static String exhaustiveSwitch(DeliveryStatus status) {
return switch(status) {
case IN_TRANSIT -> "Order is on its way";
case DELIVERED -> "Order successfully delivered";
case CANCELED -> "Order canceled";
};
}
Removing one of the case labels will lead to a compile-time error. There’s logic to that. Suppose we extend our enum with additional values. Without the requirement for exhaustiveness, we could get away with not adding new values to switch
, which will result in undesired application behavior.
So, if you add new constants to your enum class without recompiling the class containing the switch
expression, the compiler will throw an exception.
The same applies to sealed classes and interfaces that restrict what other classes or interfaces can extend or implement them. For example, we have a sealed class Vehicle
that lets only Motorcycle
, Car
, and Van
extend it:
public static abstract sealed class Vehicle permits Motorcycle, Car, Van {
int maxSpeed;
double maxPermittedWeight;
abstract String printInfo();
}
public static final class Motorcycle extends Vehicle {
int maxSpeed = 140;
double maxPermittedWeight = 350.5;
@Override
String printInfo() {
return "max speed: " + maxSpeed
+ "\nmax permitted weight: " + maxPermittedWeight;
}
}
public static non-sealed class Car extends Vehicle {
final int maxSpeed = 137;
final double maxPermittedWeight = 850.0;
final String fuel = "petrol";
@Override
String printInfo() {
return "max speed: " + maxSpeed
+ "\nmax permitted weight: " + maxPermittedWeight
+ "\nfuel: " + fuel;
}
}
public static final class Van extends Vehicle {
int maxSpeed = 111;
double maxPermittedWeight = 1543.0;
final int passengerSeats = 10;
@Override
String printInfo() {
return "max speed: " + maxSpeed
+ "\nmax permitted weight: " + maxPermittedWeight
+ "\npassenger seats: " + passengerSeats;
}
}
We now have to cover all classes permitted by Vehicle
in our switch
expression.
static String exhaustiveSwitchWithSealed(Vehicle vehicle) {
return switch (vehicle) {
case Motorcycle m -> m.printInfo();
case Car c -> c.printInfo();
case Van v -> v.printInfo();
};
}
But there’s a catch. If you remove one of the input values (here or in the enum example), and instead, add a default
clause like that
static String exhaustiveSwitchWithSealed(Vehicle vehicle) {
return switch (vehicle) {
case Motorcycle m -> m.printInfo();
case Car c -> c.printInfo();
default -> "No such vehicle";
};
}
The code will compile and run without errors. So, the general recommendation would be to avoid using default
clauses in switch
statements / expressions working with enums and sealed classes.
Before we move on to another section, I would like to highlight another possible pitfall related to enums in switch
.
If you use a traditional switch
statement with case …:
labels, and don’t specify all existing enum constants:
static String nonExhaustiveSwitch(DeliveryStatus status) {
String response = "";
switch (status) {
case IN_TRANSIT:
response = "Order is on its way";
break;
case DELIVERED:
response = "Order successfully delivered";
break;
}
return response;
}
The code will also compile and run without errors. In case you use Intellij IDEA, Nicolai Parlog offered a workaround to this issue. Go to Settings -> Editor -> Inspections -> Enum ‘switch’ statement that misses case, and set the severity level to Error. Apply the changes. After that, you will see that the incomplete switch statement is highlighted by the IDE.
Using record patterns with switch
Record patterns combine the power of Java records (classes that act as immutable data carriers) and pattern matching.
A record pattern includes a record class type and a pattern list matched against the record component values.
For instance, we have a record Rectangle
:
record Rectangle(double a, double b) { }
Then the corresponding record pattern will be Rectangle(double a, double b)
. Now, we can use this record pattern with the instanceof
operator: without creating local variables and extracting the components explicitly, we can access the value components directly:
static double getArea(Object shape) {
if (shape instanceof Rectangle(double a, double b)) {
return a * b;
} else throw new IllegalArgumentException("Unknown shape");
}
In a simple case without complex hierarchies, record patterns in switch
statements / expressions help us to write very laconic code:
static double switchWithRecordPattern(Object shape) {
return switch (shape) {
case Rectangle(double a, double b) -> a + b;
case Square(double a) -> a * a;
default -> throw new IllegalStateException("Unexpected value: " + shape);
};
}
record Rectangle(double a, double b) { }
record Square(double a) { }
In case you have a more complex situation with hierarchy involved:
class Tesla {}
class ModelY extends Tesla {}
record Selection<T> (T first, T second) { }
You must make sure that the switch
statement / expression is exhaustive, i.e., all possible combinations of the component pattern types must be covered:
static String switchRecordExhaustive(Selection<Tesla> car) {
return switch (car) {
case Selection<Tesla> (ModelY modelY1, ModelY modelY2) -> "ModelY and ModelY";
case Selection<Tesla> (Tesla tesla, ModelY modelY) -> "Tesla and ModelY";
case Selection<Tesla> (ModelY modelY, Tesla tesla) -> "ModelY and Tesla";
case Selection<Tesla> (Tesla tesla1, Tesla tesla2) -> "Tesla and Tesla";
};
}
Conclusion
Pattern matching for switch is a useful feature that makes the code more concise, expressive, and reliable.
Subscribe to our newsletter if you want to learn about other cool features in newer Java versions!