Java Switch Expression
Java switch expressions return a value, avoid fallthrough, and enforce exhaustiveness. See arrow syntax, yield, pattern matching, null handling, and practical examples in Java.
Java switch expressions let a switch produce a value instead of only performing side effects. They use arrow syntax (->), avoid fallthrough, and require the compiler to verify that all possible inputs are covered. This guide explains their syntax, exhaustiveness rules, use with type patterns, and how they compare to traditional switch statements.
The Basic Syntax
The traditional switch statement in Java falls through by default and requires break statements. A switch expression evaluates to a value, supports arrow syntax or the older colon labels, and eliminates fallthrough-related bugs.
Consider a switch statement:
String result; switch (day) { case MONDAY: case FRIDAY: result = "Work from home"; break; case SATURDAY: case SUNDAY: result = "Weekend"; break; default: result = "Office"; }
The same logic as a switch expression:
String result = switch (day) { case MONDAY, FRIDAY -> "Work from home"; case SATURDAY, SUNDAY -> "Weekend"; default -> "Office"; };
The -> separates a case label from its result, and the expression is assigned directly to a variable. There is no fallthrough. When a case needs multiple statements, use a block and return a value with yield:
int num = switch (day) { case MONDAY, FRIDAY -> { int hours = 8; yield hours * 1; } case SATURDAY, SUNDAY -> 0; default -> 8; };
yield produces the value of the switch expression from a block. It is valid only inside a switch expression, not inside a switch statement.
Where Switch Expressions Differ from Switch Statements
The most important difference is that a switch expression produces a value. That changes how code is structured and what the compiler can verify.
- A switch statement performs side effects, and an unlisted input simply does nothing unless a
defaultbranch exists. - A switch expression returns a result and must be exhaustive. For every non-null value that the switch can accept, a result must be produced; null handling is a separate concern covered below.
For an enum, exhaustiveness is checked at compile time. If you switch over an enum and omit both a case and a default, the code does not compile:
enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY } String type = switch (day) { case SATURDAY, SUNDAY -> "weekend"; case MONDAY, FRIDAY -> "office"; default -> "remote"; };
Remove the default and omit any day, and the compiler rejects the code. This is a significant improvement over a switch statement, where a missing case is not reported as an error.
Arrow Syntax vs. Colon Syntax
A switch expression can also use the traditional colon labels combined with yield:
String result = switch (day) { case MONDAY: case FRIDAY: yield "Work from home"; case SATURDAY: case SUNDAY: yield "Weekend"; default: yield "Office"; };
The arrow form is more concise and is generally preferred for new code. Use the colon form if you need to follow an existing codebase style or if you are maintaining code that already uses colon labels.
Exhaustiveness and the Default Case
Exhaustiveness is one of the strongest reasons to use a switch expression. With an enum, the compiler knows all possible constants. Without a default, every constant must have a corresponding case.
For int and String selectors, there is no finite set of possible values, so a default is required:
int value = switch (input) { case "a" -> 1; case "b" -> 2; default -> 0; };
If you remove the default, the compiler rejects the code. That forces you to handle unexpected input explicitly.
Runtime Behavior and Performance
The runtime cost of a switch expression is essentially the same as the runtime cost of a switch statement. Both compile to similar bytecode and can use efficient lookup instructions. Arrow syntax and yield by themselves do not change that. Choose between a switch statement and a switch expression based on clarity and compile-time checks, not performance.
The selector expression is evaluated exactly once, just as in a switch statement. If the selector is a method call, that method is invoked once and the result is used for all case comparisons.
Pattern Matching in Switch Expressions
Type patterns for switch were previewed in Java 17 and became standard in Java 21. They let you match on the type of the selector and bind a variable:
Object obj = ...; String result = switch (obj) { case Integer i -> "Integer: " + i; case String s -> "String: " + s; default -> "Unknown"; };
This is especially useful with sealed hierarchies. If you declare a sealed interface and its permitted implementations, the compiler can verify that an exhaustive switch covers all subtypes without a default:
sealed interface Shape permits Circle, Rectangle, Triangle {} record Circle(double radius) implements Shape {} record Rectangle(double w, double h) implements Shape {} record Triangle(double base, double height) implements Shape {} String describe(Shape s) { return switch (s) { case Circle c -> "Circle"; case Rectangle r -> "Rectangle"; case Triangle t -> "Triangle"; }; }
Here, all permitted subtypes of Shape are covered, so the switch is exhaustive without a default. If you add a new permitted subtype, the compiler forces every exhaustive switch over Shape to be updated. That is a useful maintainability check.
These pattern-matching examples require Java 21 or later; in Java 17 they were available only as a preview feature.
Commonly Missed Restrictions
Switch expressions do not replace every conditional construct. Several restrictions still apply:
- A value-based switch selector, without pattern labels, must be of type
char,byte,short,int, one of the corresponding wrapper classes, an enum, orString. You cannot switch on a primitivelongorfloatas a selector. With type patterns, the selector can instead be a reference type such asObjector a sealed type. - Value
caselabels must be compile-time constants. Afinallocal variable initialized from a method call is not constant and cannot be used as a case label. yieldis used only in block cases. A simple arrow case already produces a value and does not useyield.breakis not allowed inside a switch expression; the way to produce a value from a block isyield.
Where Switch Expressions Are Worth Using
Use a switch expression when you need to map an input to an output value and every possible result is known at compile time. Good examples include enum mapping, string-based command dispatch, and type-based processing over sealed hierarchies.
Avoid a switch expression when the only goal is a sequence of side effects. A switch statement is more honest there because it does not force you to produce a value. You can still write a block that performs side effects and then yields a value, but if the returned value is not used, a switch statement is clearer.
Switch expressions also work well inside lambdas. You can build a Function whose body is a switch expression:
Function<Day, String> dayType = day -> switch (day) { case MONDAY, TUESDAY, WEDNESDAY, THURSDAY -> "workday"; case FRIDAY -> "short-day"; case SATURDAY, SUNDAY -> "weekend"; };
This compiles because the switch covers every Day constant. It is a concise implementation of the function's apply method.
A Practical Example That Combines Concepts
Consider a payment-type enum where you want to calculate a discount:
enum PaymentType { CREDIT_CARD, DEBIT_CARD, PAYPAL, BITCOIN } double discount(PaymentType type) { return switch (type) { case CREDIT_CARD -> 0.02; case DEBIT_CARD -> 0.0; case PAYPAL -> 0.05; case BITCOIN -> 0.10; }; }
The compiler verifies that all four payment types are present. If you add a new payment type, the compiler forces every exhaustive switch to be updated, preventing silent missing behavior in business logic.
JVM Bytecode: Tableswitch and Lookupswitch
For switches over integers, the JVM may use tableswitch when case values are dense or lookupswitch when they are sparse. Both instructions are efficient: tableswitch uses an index lookup, and lookupswitch uses a search over sorted case values. Switch expressions have the same bytecode-level behavior as switch statements, so there is no reason to reorder cases manually for performance.
Null Handling
A switch over a reference value throws NullPointerException if the selector is null and no null case exists. Before Java 21, case null was not a standard feature, so code that needed to compile on older releases had to guard against null before the switch:
String result = input == null ? "default" : switch (input) { case "x" -> "y"; default -> "z"; };
In Java 21 and later, pattern matching for switch is standard and a case null label is allowed:
String result = switch (input) { case null -> "default"; case "x" -> "y"; default -> "z"; };
This is useful at system boundaries where null values are common.
Avoiding Common Bugs When Refactoring
When converting a switch statement to a switch expression, the most common mistake is leaving break in the code. break is not allowed inside a switch expression. Use arrow syntax or yield in a block.
Every branch must produce a value. If you use a block, the compiler requires that yield is reachable on all paths. The compiler enforces this, but it can still be surprising when a block grows multiple statements.
When a switch expression is the body of a lambda, use a block if the lambda needs more statements:
Function<Day, String> f = day -> { int hour = getHour(); return switch (day) { case SATURDAY, SUNDAY -> "Weekend"; default -> "Workday"; }; };
This compiles because the switch expression is an expression inside the lambda's block, and the lambda returns its value.
Compatibility with Java Versions
Switch expressions went through previews in Java 12 and 13 and were finalized in Java 14. From Java 14 onward, they are available without compiler flags. Type patterns for switch were previewed in Java 17 and finalized in Java 21, and sealed classes were finalized in Java 17.
If your project must compile with Java 13 or earlier, switch expressions were available only with preview features. For production code, use Java 14 or newer for basic switch expressions, and Java 21 or newer for the pattern-matching and case null examples in this article. Because switch expressions are part of the finalized language, there are no vendor-specific differences in core syntax once the feature is final.