A Developer’s Guide to Java’s Enhanced Switch Expression

Reetesh Kumar
9 min readOct 17, 2023

--

Introduction

In Java 12, enhanced switch expressions were introduced as a preview feature, eventually becoming a standard feature in Java 16. These improved switch expressions offer a multitude of advantages compared to the traditional switch statements. They offer a number of improvements over traditional switch statements, including:

  1. Arrow syntax: The new arrow syntax (case L ->) is more concise and easier to read than the traditional colon syntax (case L:).
  2. Multiple values per case: We can now specify multiple values per case, using commas to separate them. This eliminates the need to use fall-through cases.
  3. Yield statement: We can use the yield keyword to return a value from a switch expression. This makes it easier to use switch expressions in expressions and assignments.

In this article, we’ll discuss the challenges associated with the traditional switch statements, delve into what switch expressions are, and highlight their benefits for developers.

Traditional Switch Statements

When envisioning a switch construct as a multi-faceted conditional, utilizing an expression appears more appropriate. Yet, up to this point, the switch was solely usable as a statement. The existing switch structure is limited and wordy, frequently resulting in error-ridden code that poses debugging challenges.

Here’s a traditional switch statement example in Java that returns an interest rate based on different account types:

public double getInterestRate(AccountType accountType) {
double interestRate;

switch (accountType) {
case SAVINGS:
interestRate = 0.02; // 2% for Savings Account
break;
case CHECKING:
interestRate = 0.015; // 1.5% for Checking Account
// Missing break statement
case CREDIT:
interestRate = 0.03; // 3% for Business Account
break;
default:
interestRate = 0.01; // 1% for all other account types
break;
}

return interestRate;
}

The preceding code has multiple issues:

1. Repetitive break and assignment statements clutter the code.

2. The excessive use of code verbosity can impede code comprehension.

3. Default fall-through in switch branches sneaks in a logical error — the missing break statement for case label CHECKING lets the control fall through to case label CREDIT. This results in an assignment of 0.03 instead of 0.015 to interestRate when we execute getInterestRate(AccountType accountType).

Switch expressions

Let’s rewrite the preceding example using a switch expression.

public class Bank {
public enum AccountType {
SAVINGS,
CHECKING,
CREDIT
}
public double getInterestRate(AccountType accountType) {
return switch (accountType) {
case SAVINGS -> 0.02; // 2% for Savings Account
case CHECKING -> 0.015; // 1.5% for Checking Account
case CREDIT -> 0.03; // 3% for Business Account
default -> 0.01; // 1% for all other account types
};
}

This switch expression returns the interest rate for a bank account based on its type. The default case is required to ensure that a value is always returned, even if the input account type is not known.

The preceding code offers multiple benefits:

  1. The code in a switch branch is concise and easy to read. We define what to execute to the right of ->.
  2. Switch branches have the ability to return values, facilitating variable assignments.
  3. There’s no need for a break statement to indicate the end of a switch branch. Without a break, the control won’t unintentionally continue through the switch labels, reducing the risk of logical mistakes.

Here is an example of how to use the getInterestRate() method:

Bank bank = new Bank();

double savingsInterestRate = bank.getInterestRate(Bank.AccountType.SAVINGS);
double checkingInterestRate = bank.getInterestRate(Bank.AccountType.CHECKING);
double creditInterestRate = bank.getInterestRate(Bank.AccountType.CREDIT);

System.out.println("Savings interest rate: " + savingsInterestRate);
System.out.println("Checking interest rate: " + checkingInterestRate);
System.out.println("Credit interest rate: " + creditInterestRate);

// Output

Savings interest rate: 0.02
Checking interest rate: 0.015
Credit interest rate: 0.03

Enhanced switch expressions can also be used in more complex expressions. For example, the following code calculates the annual interest for a bank account based on its type and balance:

double calculateAnnualInterest(Bank.AccountType accountType, double balance) {
return switch (accountType) {
case SAVINGS -> balance * 0.02;
case CHECKING -> balance * 0.015;
case CREDIT -> balance * 0.03;
default -> throw new IllegalArgumentException("Unknown account type: " + accountType);
};
}

Here is an example of how to use the calculateAnnualInterest() method:

double savingsAnnualInterest = calculateAnnualInterest(Bank.AccountType.SAVINGS, 1000.0);
double checkingAnnualInterest = calculateAnnualInterest(Bank.AccountType.CHECKING, 500.0);
double creditAnnualInterest = calculateAnnualInterest(Bank.AccountType.CREDIT, 2000.0);

System.out.println("Savings annual interest: " + savingsAnnualInterest);
System.out.println("Checking annual interest: " + checkingAnnualInterest);
System.out.println("Credit annual interest: " + creditAnnualInterest);

// Output

Savings annual interest: 20.0
Checking annual interest: 7.5
Credit annual interest: 60.0

We Can Define multiple values in the same case label

We can now specify multiple values per case, using commas to separate them. This eliminates the need to use fall-through cases. Here is an example of using multiple case values in a switch statement for a bank class example:

public class Bank {
public enum AccountType {
SAVINGS,
CHECKING,
CREDIT
}
public double getInterestRate(AccountType accountType) {
return switch (accountType) {
case SAVINGS, CHECKING -> 0.1;
case CREDIT -> -0.2;
default -> throw new IllegalArgumentException("Unknown account type: " + accountType);
};
}
}

In this example, the switch statement matches the accountType variable against multiple case values: SAVINGS and CHECKING. If the accountType variable is equal to either of these values, the switch statement returns an interest rate of 0.1. If the accountType variable is equal to CREDIT, the switch statement returns an interest rate of -0.2. Otherwise, the switch statement throws an IllegalArgumentException.

Here is an example of how to use the getInterestRate() method:

Bank bank = new Bank();
double savingsInterestRate = bank.getInterestRate(Bank.AccountType.SAVINGS);
double checkingInterestRate = bank.getInterestRate(Bank.AccountType.CHECKING);
System.out.println("Savings interest rate: " + savingsInterestRate); // 0.1
System.out.println("Checking interest rate: " + checkingInterestRate); // 0.1

As we can see, the switch statement was able to match the accountType variable against multiple case values and return the correct interest rate for each account type.

This switch statement is more efficient than using multiple if statements, because it only needs to evaluate the accountType variable once. Multiple case values in switch statements can be a useful way to make our code more concise and efficient.

Yield Statement

The yield statement allows us to return a value from a switch expression. This is useful if we need to use the switch expression in an expression or assignment. A yield statement transfers control by causing an enclosing switch expression to produce a specified value.

Difference between return and yield in switch expression

The main difference between return and yield in switch expressions is that return terminates the switch expression, while yield returns a value from the switch expression but does not terminate it.

In other words, if we use return in a switch expression, the switch expression will immediately evaluate and return the value that you specified. If we use yield in a switch expression, the switch expression will return the value that we specified, but it will continue to evaluate the remaining cases in the switch expression.

yield Specification

1. A yield statement attempts to transfer control to the innermost enclosing switch expression; this expression, which is called the yield target, then immediately completes normally, and the value of the Expression becomes the value of the switch expression.

2. It is a compile-time error if a yield statement has no yield target.

3. It is a compile-time error if the yield target contains any method, constructor, initializer, or lambda expression that encloses the yield statement.

4. It is a compile-time error if the Expression of yield statement is void.

5. Execution of a yield statement first evaluates the Expression. If the evaluation of the Expression completes abruptly for some reason, then the yield statement completes abruptly for that reason. If the evaluation of the Expression completes normally, producing a value V, then the yield statement completes abruptly, the reason being a yield with value V.

For example, the following code calculates the annual interest for a bank account based on its type and balance:

double calculateAnnualInterest(Bank.AccountType accountType, double balance) {
return switch (accountType) {
case SAVINGS -> yield balance * 0.1;
case CHECKING -> yield balance * 0.05;
case CREDIT -> yield balance * -0.2;
default -> throw new IllegalArgumentException("Unknown account type: " + accountType);
};
}

Here is an example of how to use the calculateAnnualInterest() method:

double savingsAnnualInterest = calculateAnnualInterest(Bank.AccountType.SAVINGS, 1000.0);
double checkingAnnualInterest = calculateAnnualInterest(Bank.AccountType.CHECKING, 500.0);
double creditAnnualInterest = calculateAnnualInterest(Bank.AccountType.CREDIT, -2000.0);

System.out.println("Savings annual interest: " + savingsAnnualInterest); // 100.0
System.out.println("Checking annual interest: " + checkingAnnualInterest); // 25.0
System.out.println("Credit annual interest: " + creditAnnualInterest); // 400.0

Pattern matching for switch statements and expressions

Pattern matching for the switch was introduced in JDK 17, refined in JDK 18, 19, and 20, and finalized in JDK 21.

It allows us to match the value of a variable against a pattern and execute the corresponding code. This can make our code more concise and expressive, and it can also help to reduce the number of bugs in the code. To use pattern matching in a switch expression, we use the case keyword followed by a pattern.

The pattern can be a simple value, such as a string or a number, or it can be a more complex expression, such as a regular expression or a record. If the value of the variable matches the pattern, the corresponding code is executed. Otherwise, the next case is evaluated.

Here are a few pattern-matching examples using different expression types:

  1. Selector Expression Type

The type of a selector expression can either be an integral primitive type or any reference type. The following 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 Bank(int accountNumber, double balance) { }
enum AccountType { SAVINGS, CREDIT, BUSINESS; }
...
static void typeTester(Object obj) {
switch (obj) {
case null -> System.out.println("null");
case String s -> System.out.println("String");
case AccountType a -> System.out.println("Account type with " + c.values().length + " values");
case Bank b -> System.out.println("Record class: " + b.toString());
case int[] ia -> System.out.println("Array of int values of length" + ia.length);
default -> System.out.println("Something else");
}
}

2. When Clauses

A when clause enables a pattern to be refined with a Boolean expression. A pattern label that contains a when clause is called a guarded pattern label, and the Boolean expression in the when clause is called a guard. A value matches a guarded pattern label if it matches the pattern and the guard evaluates to true. Consider the following example:

    static void test(Object obj) {
switch (obj) {
case String s:
if (s.length() == 1) {
System.out.println("Short: " + s);
} else {
System.out.println(s);
}
break;
default:
System.out.println("Not a string");
}
}

We can move the Boolean expression s.length == 1 into the case label with a when clause:

  static void test(Object obj) {
switch (obj) {
case String s when s.length() == 1 -> System.out.println("Short: " + s);
case String s -> System.out.println(s);
default -> System.out.println("Not a string");
}
}

The first pattern label (which is a guarded pattern label) matches if obj is both a String and of length 1. The second pattern label matches if obj is a String of a different length.

A guarded pattern label has the form p when e where p is a pattern and e is a Boolean expression.

3. Pattern Label Dominance

It’s possible that many pattern labels could match the value of the selector expression. To help predictability, the labels are tested in the order that they appear in the 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.

4. Null case Labels

Prior to this feature, 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.

Conclusion

Overall, enhanced switch expressions are a significant improvement over traditional switch statements. They are more concise, easier to read, and more powerful. Here are some additional benefits of using enhanced switch expressions:

  • They make code more expressive and maintainable.
  • They can help to reduce the number of bugs in code.
  • They can improve the performance of code, especially for switch statements with a large number of cases.

If you are using Java 16 or later, I encourage you to start using enhanced switch expressions in your code.

Happy Learning !!!

--

--

Reetesh Kumar

Software developer. Writing about Java, Spring, Cloud and new technologies. LinkedIn: www.linkedin.com/in/reeteshkumar1