Patterns in switch
Since Java 21 a switch can check not only specific values but also types. Each case holds a pattern, as in instanceof, and the first matching one runs. Long if-else chains with instanceof turn into a short, clear switch.
Type patterns in case
Let's rewrite the example from the previous lesson. The switch returns a value, so it can go straight into return:
String describe(Object value) {
return switch (value) {
case Integer number -> "an integer " + number;
case String text -> "the string \"" + text + "\"";
default -> "something else";
};
}
void main() {
IO.println(describe(42));
IO.println(describe("Anna"));
IO.println(describe(2.5));
}Output:
an integer 42
the string "Anna"
something elseThe default branch is required: an Object can be anything, and the switch must know what to do in every case.
when guards
You can add a condition to a pattern with when. The branch runs only if both the type and the condition match:
String classify(Object value) {
return switch (value) {
case Integer n when n < 0 -> "a negative number";
case Integer n when n == 0 -> "zero";
case Integer n -> "a positive number";
case String s when s.isEmpty() -> "an empty string";
case String s -> "a string";
default -> "unknown";
};
}
void main() {
IO.println(classify(-7));
IO.println(classify(0));
IO.println(classify(15));
IO.println(classify(""));
}Output:
a negative number
zero
a positive number
an empty stringBranches are checked from top to bottom, so the specific cases with when go before the general ones.
case null
An ordinary switch throws NullPointerException on null. To handle null explicitly, add a case null branch:
String check(Object value) {
return switch (value) {
case null -> "no value";
case String s -> "the string " + s;
default -> "something else";
};
}
void main() {
IO.println(check(null));
IO.println(check("ok"));
}Output:
no value
the string okSealed hierarchies
When a switch goes over the subtypes of a sealed interface, default isn't needed: the compiler knows the full list of subtypes and checks that none is forgotten. This is called exhaustiveness:
sealed interface Shape permits Circle, Square, Rectangle {}
record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s -> s.side() * s.side();
case Rectangle r -> r.width() * r.height();
};
}
void main() {
IO.println(area(new Square(3)));
IO.println(area(new Rectangle(2, 5)));
IO.println(Math.round(area(new Circle(1)) * 100) / 100.0);
}Output:
9.0
10.0
3.14If you add a new shape later, every such switch stops compiling until it gets a new branch. The mistake is caught before the program runs.
Exhaustiveness for enums
The exhaustiveness check works for enums too: if a switch covers all the values, default isn't needed. If you forget one, the compiler reports an error. Here all three are covered:
enum Level { LOW, MEDIUM, HIGH }
String color(Level level) {
return switch (level) {
case LOW -> "green";
case MEDIUM -> "yellow";
case HIGH -> "red";
};
}
void main() {
IO.println(color(Level.MEDIUM));
}Output:
yellowCommon mistake
If a more general pattern sits above a specific one, the specific one can never match. The compiler calls this dominance and reports an error:
String describe(Object value) {
return switch (value) {
case CharSequence cs -> "text";
case String s -> "a string";
default -> "something else";
};
}
void main() {
IO.println(describe("Oleg"));
}Output:
Main.java:4: error: this case label is dominated by a preceding case label
case String s -> "a string";
^
1 error
error: compilation failedString is a kind of CharSequence, so the first branch takes all strings. Put specific patterns above general ones.