6 Pattern Matching for switch Expressions and Statements
A switch
statement transfers control to one of several
statements or expressions, depending on the value of its selector expression. In earlier
releases, the selector expression must evaluate to a number, string or enum
constant, and case labels must be constants. However, in this release, the selector
expression can be of any type, and case
labels can have patterns.
Consequently, a switch
statement or expression can test whether its
selector expression matches a pattern, which offers more flexibility and expressiveness
compared to testing whether its selector expression is exactly equal to a
constant.
Note:
This is a preview feature. A preview feature is a feature whose design, specification, and implementation are complete, but is not permanent. A preview feature may exist in a different form or not at all in future Java SE releases. To compile and run code that contains preview features, you must specify additional command-line options. See Preview Language and VM Features.
For background information about pattern matching for
switch
expressions and statements, see JEP 427.
Consider the following code that calculates the perimeter of certain shapes from the section Pattern Matching for instanceof Operator:
interface Shape { }
record Rectangle(double length, double width) implements Shape { }
record Circle(double radius) implements Shape { }
...
public static double getPerimeter(Shape shape) throws IllegalArgumentException {
if (shape instanceof Rectangle r) {
return 2 * r.length() + 2 * r.width();
} else if (shape instanceof Circle c) {
return 2 * c.radius() * Math.PI;
} else {
throw new IllegalArgumentException("Unrecognized shape");
}
}
You can rewrite this code to use a pattern switch
expression
as follows:
public static double getPerimeter(Shape shape) throws IllegalArgumentException {
return switch (shape) {
case Rectangle r -> 2 * r.length() + 2 * r.width();
case Circle c -> 2 * c.radius() * Math.PI;
default -> throw new IllegalArgumentException("Unrecognized shape");
};
}
The following example uses a switch
statement instead of a
switch
expression:
public static double getPerimeter(Shape shape) throws IllegalArgumentException {
switch (shape) {
case Rectangle r: return 2 * r.length() + 2 * r.width();
case Circle c: return 2 * c.radius() * Math.PI;
default: throw new IllegalArgumentException("Unrecognized shape");
}
}
Selector Expression Type
switch
expression matches the selector expression
obj
with type patterns that involve a class type, an enum type, a
record type, and an array type:
record Point(int x, int y) { }
enum Color { RED, GREEN, BLUE; }
...
static void typeTester(Object obj) {
switch (obj) {
case null -> System.out.println("null");
case String s -> System.out.println("String");
case Color c -> System.out.println("Color with " + c.values().length + " values");
case Point p -> System.out.println("Record class: " + p.toString());
case int[] ia -> System.out.println("Array of int values of length" + ia.length);
default -> System.out.println("Something else");
}
}
Pattern Label Dominance
switch
block. In addition, the compiler raises an
error if a pattern label can never match because a preceding one will always match
first. The following example results in a compile-time error:
static void error(Object obj) {
switch(obj) {
case CharSequence cs ->
System.out.println("A sequence of length " + cs.length());
case String s -> // error: this case label is dominated by a preceding case label
System.out.println("A string: " + s);
default ->
throw new IllegalStateException("Invalid argument");
}
}
The first pattern label case CharSequence cs
dominates the second pattern label case String s
because every
value that matches the pattern String s
also matches the pattern
CharSequence cs
but not the other way around. It's because
String
is a subtype of CharSequence
.
A pattern label can dominate a constant label. These examples cause compile-time errors:
static void error2(Integer value) {
switch(value) {
case Integer i ->
System.out.println("Integer: " + i);
case -1, 1 -> // Compile-time errors for both cases -1 and 1:
// this case label is dominated by a preceding case label
System.out.println("The number 42");
default ->
throw new IllegalStateException("Invalid argument");
}
}
enum Color { RED, GREEN, BLUE; }
static void error3(Color value) {
switch(value) {
case Color c ->
System.out.println("Color: " + c);
case RED -> // error: this case label is dominated by a preceding case label
System.out.println("The color red");
}
}
A guarded pattern label (see When Clauses) can also dominate a constant label:
static void error4(Integer value) {
switch(value) {
case Integer i when i > 0 ->
System.out.println("Positive integer");
case -1, 1 -> // Compile-time errors for both cases -1 and 1:
// this case label is dominated by a preceding case label
System.out.println("Value is 1 or -1");
default ->
throw new IllegalStateException("Invalid argument");
}
}
Even though the guarded pattern label case Integer i when i > 0
doesn't
match the value -1
, the compiler still generates an error.
To resolve these compiler errors related to dominance, ensure that constant labels appear before guarded pattern labels, which must appear before non-guarded type pattern labels:
static void checkIntegers(Integer value) {
switch(value) {
case -1, 1 -> // Constant labels
System.out.println("Value is 1 or -1");
case Integer i when i > 0 -> // Guarded pattern label
System.out.println("Positive integer");
case Integer i -> // Non-guarded type pattern label
System.out.println("Neither positive, 1, nor -1");
}
}
Type Coverage in switch Expressions and Statements
As described in Switch Expressions, the switch
blocks of switch
expressions and switch
statements, which use pattern or
null
labels, must be exhaustive. This means that for all possible
values, there must be a matching switch
label. The following
switch
expression is not exhaustive and generates a compile-time
error. Its type coverage consists of the subtypes of String
or
Integer
, which doesn't include the type of the selector expression,
Object:
static int coverage(Object obj) {
return switch (obj) { // Error - not exhaustive
case String s -> s.length();
case Integer i -> i;
};
}
However, the type coverage of the case label default
is all
types, so the following example compiles:
static int coverage(Object obj) {
return switch (obj) {
case String s -> s.length();
case Integer i -> i;
default -> 0;
};
}
The compiler takes into account whether the type of a selector expression is
a sealed class. The following switch
expression compiles. It doesn't
need a default
case label because its type coverage is the classes
A
, B
, and C
, which are the only
permitted subclasses of S
, the type of the selector expression:
sealed interface S permits A, B, C { }
final class A implements S { }
final class B implements S { }
record C(int i) implements S { } // Implicitly final
...
static int testSealedCoverage(S s) {
return switch (s) {
case A a -> 1;
case B b -> 2;
case C c -> 3;
};
}
The compiler can also determine the type coverage of a
switch
expression or statement if the type of its selector
expression is a generic sealed class. The following example compiles. The only permitted
subclasses of interface I
are classes A
and
B
. However, because the selector expression is of type
I<Integer>
, the switch
block requires only
class B
in its type coverage to be exhaustive:
sealed interface I<T> permits A, B {}
final class A<X> implements I<String> {}
final class B<Y> implements I<Y> {}
static int testGenericSealedExhaustive(I<Integer> i) {
return switch (i) {
// Exhaustive as no A case possible!
case B<Integer> bi -> 42;
};
}
If a switch
expression or statement is exhaustive at
compile time but not at run time, then a MatchException
is
thrown. This can happen when a class that contains an exhaustive switch
expression or statement has been compiled, but a sealed hierarchy that is used in the
analysis of the switch
expression or statement has been subsequently
changed and recompiled. Such changes are migration incompatible and may lead to a
MatchException being thrown when running the
switch
statement or expression. Consequently, you need to recompile
the class containing the switch
expression or statement.
Consider the following two classes ME
and
Seal
:
class ME {
public static void main(String[] args) {
System.out.println(switch (Seal.getAValue()) {
case A a -> 1;
case B b -> 2;
});
}
}
sealed interface Seal permits A, B {
static Seal getAValue() {
return new A();
}
}
final class A implements Seal {}
final class B implements Seal {}
The switch
expression in the class ME
is
exhaustive and this example compiles. When you run ME
, it prints the
value 1
. However, suppose you edit Seal
as follows and
compile this class and not
ME
:
sealed interface Seal permits A, B, C {
static Seal getAValue() {
return new A();
}
}
final class A implements Seal {}
final class B implements Seal {}
final class C implements Seal {}
When you run ME
, it throws a MatchException:
Exception in thread "main" java.lang.MatchException
at ME.main(ME.java:3)
Scope of Pattern Variable Declarations
As described in the section Pattern Matching for instanceof Operator, the scope of a pattern variable is the places where the program can
reach only if the instanceof
operator is true
:
public static double getPerimeter(Shape shape) throws IllegalArgumentException {
if (shape instanceof Rectangle s) {
// You can use the pattern variable s of type Rectangle here.
} else if (shape instanceof Circle s) {
// You can use the pattern variable s of type Circle here
// but not the pattern variable s of type Rectangle.
} else {
// You cannot use either pattern variable here.
}
}
In a switch
expression, you can use a pattern variable
inside the expression, block, or throw
statement that appears right of
the arrow. For example:
static void test(Object obj) {
switch (obj) {
case Character c -> {
if (c.charValue() == 7) {
System.out.println("Ding!");
}
System.out.println("Character, value " + c.charValue());
}
case Integer i ->
System.out.println("Integer: " + i);
default ->
throw new IllegalStateException("Invalid argument");
}
}
The scope of pattern variable c
is the block to the right of
case Character c ->
. The scope of pattern variable
i
is the println
statement to the right of
case Integer I ->
.
In a switch
statement, you can use a case label's pattern
variable in its switch
-labeled statement group. However, you can't use
it in any other switch
-labeled statement group, even if the program
flow can fall through a default
statement group. For example:
static void test(Object obj) {
switch (obj) {
case Character c:
if (c.charValue() == 7) {
System.out.print("Ding ");
}
if (c.charValue() == 9) {
System.out.print("Tab ");
}
System.out.println("character, value " + c.charValue());
default:
// You cannot use the pattern variable c here:
throw new IllegalStateException("Invalid argument");
}
}
The scope of pattern variable c
consists of the
case Character c
statement group: the two if
statements and the println
statement that follows them. The scope
doesn't include the default
statement group even though the
switch
statement can execute the case Character c
statement group, fall through the default
case label, and then execute
the default
statement group.
You will get a compile-time error if it's possible to fall through a case label that declares a pattern variable. The following example doesn't compile:
static void test(Object obj) {
switch (obj) {
case Character c:
if (c.charValue() == 7) {
System.out.print("Ding ");
}
if (c.charValue() == 9) {
System.out.print("Tab ");
}
System.out.println("character");
case Integer i: // Compile-time error
System.out.println("An integer " + i);
default:
System.out.println("Neither character nor integer");
}
}
If this code were allowed, and the value of the selector expression,
obj
, was a Character
, then the
switch
statement can execute the case Character c
statement group, then fall through the case Integer i
case label, where
the pattern variable i
would have not been initialized.
c
or i
would have
been initialized depending on the value of
obj
: case Character c, Integer i: ...
case Character c, Integer i -> ...
Null case Labels
switch
expressions
and switch
statements threw a NullPointerException
if
the value of the selector expression is null
. However, to add more
flexibility, a null
case label is available:
static void test(Object obj) {
switch (obj) {
case null -> System.out.println("null!");
case String s -> System.out.println("String");
default -> System.out.println("Something else");
}
}
This example prints null!
when obj
is
null
instead of throwing a
NullPointerException
.
You can combine a null
case label with another pattern
label:
static void testStringOrNull(Object obj) {
switch (obj) {
case null, String s -> System.out.println("String: " + s);
default -> System.out.println("Something else");
}
}
If a selector expression evaluates to null
and the
switch
block does not have null
case label, then a
NullPointerException
is thrown as normal. Consider the following
switch
statement:
String s = null;
switch (s) {
case Object obj -> System.out.println("This doesn't match null");
// No null label; NullPointerException is thrown
// if s is null
}
Although the pattern label case Object obj
matches objects
of type String, this example throws a
NullPointerException
. The selector expression evaluates to
null
, and the switch
expression doesn't contain a
null
case label.