A Developer’s Guide to Java’s Enhanced Switch Expression
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:
- Arrow syntax: The new arrow syntax (
case L ->
) is more concise and easier to read than the traditional colon syntax (case L:
). - 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.
- 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:
- The code in a switch branch is concise and easy to read. We define what to execute to the right of ->.
- Switch branches have the ability to return values, facilitating variable assignments.
- 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
andyield
in switch expressions is thatreturn
terminates the switch expression, whileyield
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 useyield
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 theExpression
becomes the value of theswitch
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
ofyield
statement is void.5. Execution of a
yield
statement first evaluates theExpression
. If the evaluation of theExpression
completes abruptly for some reason, then theyield
statement completes abruptly for that reason. If the evaluation of theExpression
completes normally, producing a valueV
, then theyield
statement completes abruptly, the reason being a yield with valueV
.
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:
- 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 !!!