This tutorial is about pattern matching for the switch statement in Java, which was first introduced in Java 17 as a preview feature.
Switch statement has been available in Java for a long time. Unfortunately, it was very limited. Before Java 17, switch only supported numeric types, enum types, and String. In addition, you can only test for exact equality against constants. Since Java 17, switch has a new feature called pattern matching which allows more flexibility for defining the condition for each case.
Using Pattern Matching for Switch
Below I am going to explain what you can do inside a switch block with the addition of the pattern matching for switch feature.
Type Patterns
Let's say you want to create a method for formatting a variable based on its type. Since the variable can have any type, the parameter has to use Object
type. However, before Java 17, switch only supported certain data types and it didn't support pattern matching for checking the type. To achieve the solution, you need to use an if...else
statement with multiple conditions.
static String formatValue(Object o) {
String result = "";
if (o instanceof Double) {
result = String.format("Double value is %f", o);
} else if (o instanceof Integer) {
result = String.format("Integer value is %d", o);
} else if (o instanceof Long) {
result = String.format("Long value is %d", o);
} else if (o instanceof String) {
result = String.format("String value is %s", o);
} else {
result = o.toString();
}
return result;
}
The above approach works as expected. However, it has some disadvantages. First, it's prone to coding errors. If you forget to assign the formatted value to the result
variable, the compiler will not be able to identify and verify that. Another disadvantage is the time complexity of O(n) even though the problem can be solved in O(1).
With the pattern matching for switch feature, it becomes possible for case labels to use pattern. The code above can be rewritten to the code below. To define a type pattern in a case label, you need to write the type that's expected for the case label followed by a variable. In the respective block, you can access the variable without the need to cast the type.
static String formatValue(Object o) {
return switch (o) {
case Double d -> String.format("Double value is %f", d);
case Integer i -> String.format("Integer value is %d", i);
case Long l -> String.format("Long value is %d", l);
case String s -> String.format("String value is %s", s);
default -> o.toString();
};
}
Guarded Patterns
If a value matches a particular type, sometimes you may need to check the passed value. For example, you want to format the passed value if it is a string whose length is greater than or equal to 1. Otherwise, if the value is a string whose length is 0, 'Empty String' will be returned.
static String formatNonEmptyString(Object o) {
switch (o) {
case String s:
if (s.length() >= 1) {
return String.format("String value is %s", s);
} else {
return "Empty string";
}
default:
return o.toString();
}
}
The solution above splits the logic to a case label (for checking the type) and an if
statement (for checking the length). If you don't link that style, it can be solved by using guarded pattern. A guarded pattern is of the form p && e
, where p
is a pattern and e
is a boolean expression. With guarded patterns, the conditional logic can be moved to the case label. The code above can be rewritten to the following
static String formatNonEmptyString(Object o) {
return switch (o) {
case String s && s.length() >= 1 -> String.format("String value is %s", s);
case String s -> String.format("Empty string");
default -> o.toString();
};
}
Parenthesized Pattern
There is another pattern called parenthesized pattern. A parenthesized pattern is of the form (p)
, where p
is a pattern. You may already be familiar with the usage of parentheses in Java. The pattern matching for switch feature also allows you to use parentheses in case labels.
For example, we want to create a case label which evaluates to true if the given value is a String whose length is at least two and contains either !
or @
. Without parentheses, it may cause ambiguity and wrong execution order.
static String formatValidString(Object o) {
return switch (o) {
case String s && s.length() >= 2 && s.contains("@") || s.contains("!") -> String.format("Valid string value is %s", s);
default -> "Invalid value";
};
}
public static void main(String[] args) {
System.out.println(formatValidString("xx")); // Invalid value
System.out.println(formatValidString("!")); // Valid string value is !
System.out.println(formatValidString("@@")); // Valid string value is @@
}
The code above returns the wrong value for the !
value because the &&
operator is evaluated first. With parenthesized pattern, you can add parentheses around s.contains("@") || s.contains("!")
, so that it will be evaluated first.
static String formatValidString(Object o) {
return switch (o) {
case String s && s.length() >= 2 && (s.contains("@") || s.contains("!")) -> String.format("Valid string value is %s", s);
default -> "Invalid value";
};
}
public static void main(String[] args) {
System.out.println(formatValidString("xx")); // Invalid value
System.out.println(formatValidString("!")); // Invalid value
System.out.println(formatValidString("@@")); // Valid string value is @@
}
Null Values Handling
Formerly, if a null
value is passed to a switch statement, a NullPointerException
will be thrown. That's because switch only supported a few reference types.
static void testNullAndSpecialValues(String s) {
if (s == null) {
System.out.println("Value is null");
return;
}
switch (s) {
case "Woolha", "Java" -> System.out.println("Special value");
default -> System.out.println("Other value");
}
}
public static void main(String[] args) {
testNullAndSpecialValues(null); // Value is null
testNullAndSpecialValues("Woolha"); // Special value
testNullAndSpecialValues("Foo"); // Other value
}
With the support of selector expression of any type and type patterns in case labels, it becomes possible to move the null checking into the switch.
static void testNullAndSpecialValues(String s) {
switch (s) {
case null -> System.out.println("Value is null");
case "Woolha", "Java" -> System.out.println("Special value");
default -> System.out.println("Other value");
}
}
public static void main(String[] args) {
testNullAndSpecialValues(null); // Value is null
testNullAndSpecialValues("Woolha"); // Special value
testNullAndSpecialValues("Foo"); // Other value
}
Completeness of pattern labels
If you create a switch expression, you have to handle all possible values. In conventional switch expressions, you can add some conditions in the switch block in order to cover all possible values. For pattern matching switch expressions, it's a bit different. If you define a switch with pattern matching, Java will check the type coverage. The case labels (including default
) are required to include the type of the selector expression.
You can take a look at the example below. The switch accepts a parameter whose type is Object
. However, there is only a case label for handling the case where the passed value is a String. There is no default
label as well.
static String formatStringValue(Object o) {
return switch (o) {
case String s -> String.format("String value is %s", s);
};
}
public static void main(String[] args) {
System.out.println(formatStringValue("test"));
}
As a result, the following error is thrown.
SwitchPatternMatching.java:125: error: the switch expression does not cover all possible input values
return switch (o) {
^
The solution is you have to ensure that the case labels cover all possible values. You can also add the default
label at the bottom.
static String formatStringValue(Object o) {
return switch (o) {
case String s -> String.format("String value is %s", s);
default -> o.toString();
};
}
public static void main(String[] args) {
System.out.println(formatStringValue("test"));
}
Dominance of Pattern Labels
In the example below, there are two switch labels. The first one evaluates to true
if the passed value is a CharSequence
. The second one evaluates to true
if the passed value is a String
. Because String
is a subclass of CharSequence
and the case label for String
is put below the one for CharSequence
, there is no chance that the execution goes to the second case label.
static void printLength(Object o) {
switch(o) {
case CharSequence cs ->
System.out.println("Sequence with length: " + cs.length());
case String s ->
System.out.println("String with length: " + s.length());
default -> System.out.println("Unknown type");
}
}
public static void main(String[] args) {
printLength("woolha");
}
The good thing is if you inadvertently make a mistake like in the code above, you'll get a compile time error.
SwitchPatternMatching.java:144: error: this case label is dominated by a preceding case label
case String s ->
Below is the fix for code above. It's valid because if the passed value is a non-String CharSequence
(StringBuilder
or StringBuffer
), the code block of the second case label will be executed.
static void printLength(Object o) {
switch(o) {
case String s ->
System.out.println("String with length: " + s.length());
case CharSequence cs ->
System.out.println("Sequence with length: " + cs.length());
default -> System.out.println("Unknown type");
}
}
public static void main(String[] args) {
printLength("woolha");
}
Summary
With the addition of pattern matching for the switch statement in Java, you can do more things inside switch statements. It enables you to perform type matching for any type, added with the support of guarded pattern and parenthesized pattern. Null values can be handled as well. In addition, the capability to check the completeness and dominance of pattern labels reduces the possibility of coding error.