Java Switch Expression
java switch expression: Learn how Java switch expressions improve on switch statements with arrow syntax, yield, and exhaustive cases. See code examples and practical...
java switch expression requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
The traditional switch statement in Java has several well-known limitations: it falls through by default, requires a break to prevent fallthrough, and its cases must be constant expressions. A switch expression changes that model. It evaluates to a value, supports both arrow syntax and traditional case labels, and can eliminate entire classes of bugs related to fallthrough and accidental assignment.
Consider the switch statement you might have written many times:
String result; switch (day) { case MONDAY: case FRIDAY: result = "Work from home"; break; case SATURDAY: case SUNDAY: result = "Weekend"; break; default: result = "Office"; }
A switch expression achieves the same logic without the result variable and without break statements:
String result = switch (day) { case MONDAY, FRIDAY -> "Work from home"; case SATURDAY, SUNDAY -> "Weekend"; default -> "Office"; };
The expression is assigned directly to a variable. The arrow -> separates a case from its value, and there is no fallthrough. If a case has more than one expression, you use a block with yield:
int num = switch (day) { case MONDAY, FRIDAY -> { int hours = 8; yield hours * 1; } case SATURDAY, SUNDAY -> 0; default -> 8; };
The yield statement returns a value from the switch expression. It is only valid inside a switch expression, not inside a switch statement.
Where the Switch Expression Differs from a Switch Statement
The most critical difference is that a switch expression produces a value. This changes how you structure code. A switch statement performs side effects; a switch expression can also perform side effects, but its primary purpose is to return a result.
Another difference is exhaustiveness. Switch expressions require that all possible input values are handled. When you switch over an enum, you must provide cases for every constant, or there must be a default. Without a default, the compiler will reject the code if the enum is not fully covered. Consider:
enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY } String type = switch (day) { case SATURDAY, SUNDAY -> "weekend"; case MONDAY, FRIDAY -> "office"; default -> "remote"; };
If you remove the default and do not list all seven days, the code will not compile. This is a compile-time guarantee, which means you cannot accidentally miss a case. This is a significant improvement over the classic switch statement, where a missing case simply falls through silently.
Choosing Between Arrow Syntax and Colon Syntax
A switch expression can also be written using the traditional colon syntax combined with yield. The arrow syntax is the more common choice for new code, but understanding the colon form is useful when reading older style or when you need to group multiple statements.
// Colon and 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 has fewer lines and is easier to read at a glance. Use the arrow form when each case produces a simple value or a block with a single yield. Use the colon form if you need to follow an established style or if you are in a codebase that already uses it.
Exhaustiveness and the Default Case
Exhaustiveness is one of the strongest arguments for using a switch expression. With an enum, the compiler knows all possible values at compile time. If you forget a case and do not have a default, the compiler will not let you compile.
For numeric types, you always need a default because there is no finite list of possible values. For example, switching over an int requires a default. The same is true for String inputs, because there is no way to enumerate all possible strings.
int value = switch (input) { case "a" -> 1; case "b" -> 2; default -> 0; };
If you accidentally remove the default here, the compiler will reject it. That is a good thing because it forces you to handle unexpected input explicitly.
Runtime Behavior and Performance
The runtime cost of a switch expression is essentially the same as that of a switch statement. The JVM compiles both to a switch-style lookup when the target types and case values support it. The arrow syntax and yield do not add measurable overhead. You should not choose between switch expressions and switch statements for performance reasons; the decision should be based on which produces clearer code and better compile-time checks.
One subtle runtime consideration is that a switch expression can capture local variables and use them in case blocks without issues, just like a switch statement. However, you must ensure that each case block terminates properly, either with a value expression or a yield. If a block falls off the end, that is a compile-time error.
Pattern Matching in Switch Expressions
Since Java 17, switch expressions support pattern matching for type patterns. This is a more advanced feature that lets you match on the type of the input and bind a variable. For example:
Object obj = ...; String result = switch (obj) { case Integer i -> "Integer: " + i; case String s -> "String: " + s; default -> "Unknown"; };
Pattern matching in switch expressions is particularly useful when dealing with sealed hierarchies. If you declare a sealed interface and a set of permitted implementations, the compiler can verify that the switch covers all cases without a default. This gives you the same exhaustiveness guarantee as with enums, but for arbitrary object types.
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, because Shape is sealed and all permitted subtypes are covered, you can omit the default and still satisfy exhaustiveness. This compiles cleanly. If you add a new subtype to the sealed hierarchy, the compiler will force you to update every exhaustive switch that handles that type. That is a powerful tool for maintainability.
Commonly Missed Restrictions
Switch expressions are not a complete replacement for all logic. Several restrictions remain:
- The selector expression (the value being switched on) must be of type
char,byte,short,int,enum,String, or one of the wrapper classes for the primitive types. You cannot switch on alongorfloatdirectly, regardless of whether you use a statement or an expression. - Each case label must be a constant expression. You cannot use a
finallocal variable that is initialized from a method call, because it is not compile-time constant. yieldis not allowed within a switch expression's case block that is simply an expression; it is only needed for blocks. Usingyieldoutside a switch expression is a compile-time error.
A common mistake is trying to use break inside a switch expression to exit early. That will not compile. The only way to produce a value from a block is yield.
Where Switch Expressions Are Worth Using
Use a switch expression whenever you need to map an input to an output value and all cases are known at compile time. This includes enum mapping, string-based command dispatch, and type-based processing with sealed hierarchies.
Avoid a switch expression when you need to perform a sequence of side effects for each case, such as logging followed by a state update. A switch statement is more appropriate there because it does not force you to produce a value. That said, you can still use a switch expression with a block that performs the side effects and then yields a value. The style choice should be based on whether the value is truly used. If the primary purpose is side effects, a statement is more honest.
Switch expressions also work well with method references and lambda-style processing. For example, you can build a Function that uses a switch expression to delegate behavior:
Function<Day, String> dayType = day -> switch (day) { case MONDAY, TUESDAY, WEDNESDAY, THURSDAY -> "workday"; case FRIDAY -> "short-day"; case SATURDAY, SUNDAY -> "weekend"; };
In this case, the switch expression is a concise way to implement the function's apply method.
A Practical Example That Combines Concepts
Consider a payment processing scenario where you have an enum PaymentType and you want to calculate a discount. You can write a switch expression that is both exhaustive and readable:
enum PaymentType { CREDIT_CARD, DEBIT_CARD, PAYPAL, BITCOIN } double discount = switch (type) { case CREDIT_CARD -> 0.02; case DEBIT_CARD -> 0.0; case PAYPAL -> 0.05; case BITCOIN -> 0.10; };
The compiler will verify that all four cases are present. If you later add a new payment type, the compiler will force you to update this expression. This prevents silent missing behavior in business logic.
When the JVM Degrades to Tableswitch or Lookupswitch
Understanding the compiled form can help you reason about performance. For a switch over integers, the JVM may use a tableswitch when the cases are dense, or a lookupswitch when they are sparse. Both are efficient with O(1) or O(log n) behavior, respectively. The same applies to switch expressions because the bytecode is similar to that of a switch statement. You should not attempt to manually optimize the ordering of cases based on this, as the JIT compiler can make its own decisions.
A more relevant performance factor is the cost of evaluating the selector expression. In a switch expression, the selector is evaluated exactly once. That is the same semantics as a switch statement. If the selector is a method call, it is invoked once and the result is used for all case comparisons. This removes a class of micro-bugs that would appear if you accidentally calling the method in each case.
How Switch Expressions Interact with Null Values
A switch expression over a reference type will throw a NullPointerException if the selector is null, just like a classic switch statement. There is no null case allowed in a switch expression, even with pattern matching. If you need to handle null, you must do it before the switch:
String result = input == null ? "default" : switch (input) { case "x" -> "y"; default -> "z"; };
Alternatively, you can make the entire switch expression return a null for a specific case, but you cannot match on null directly. This is important for production code where null values are common at boundaries.
Avoiding Common Bugs When Refactoring to Switch Expressions
When migrating from a switch statement, the most common mistake is forgetting to remove break statements. Inside a switch expression, break will not compile. You must use yield or the arrow syntax. Another common issue is accidentally returning a value from one branch and forgetting that every branch must yield a value. The compiler enforces this, but if you use a block, you must ensure that yield is reached; otherwise, the code will not compile.
Another subtle bug appears when you use a switch expression inside a lambda. Because the switch expression is now the body of the lambda, you must use a block if there are multiple 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 a statement inside the lambda's block, and the lambda returns its value.
Compatibility with Existing Java Versions
Switch expressions were introduced in Java 14 as a preview feature and became a standard feature in Java 17. If you use Java 17 or later, you can use them without any compiler flags. If you are on an earlier version, you need at least Java 14 with preview enabled (--enable-preview). For production code, you should target Java 17 LTS or newer to use switch expressions without preview limitations.
Do not use switch expressions in a codebase that must compile to older bytecode unless you are willing to enable preview features, which are not suitable for production releases. The safest approach is to upgrade to Java 17 or later.
Because switch expressions are part of the finalized language features, they are consistent across all JVM implementations. There are no vendor-specific behavior differences for the core syntax. This makes switch expressions a reliable tool for long-lived applications.