When it comes to using switch
past Java 14, there are three decisions to be made:
- colons ("classic") or arrows (since Java 14)
- statement ("classic") or expression (since Java 14)
- labels ("classic") or patterns (3rd preview in Java 19)
These are completely orthogonal
This leaves us with a whopping eight possible combinations!
Fortunately for us, these three decisions are completely orthogonal (meaning neither impacts any other), so we can examine each in isolation.
So let's do that and find out how to best use switch
in modern Java!
If you're looking for one particular decision, note that there's a table of contents that lets you skip ahead in the box on the left.
▚Colon vs Arrow
▚Colon and break
The "classic" version uses a colon after the case label (or pattern) and requires a return
or break
to prevent falling through into the next case:
// with colon and break
switch (number) {
case 1:
callMethod("one");
break;
case 2:
callMethod("two");
break;
default:
callMethod("many");
break;
}
Preventing all fall-through with break
s is the common case, but there are situations where it is intended.
Fall-through is often used to list several cases with empty branches, so they fall through into the one with statements (and usually a break
), thus applying the same behavior to all cases:
// with colon and break
switch (number) {
case 1:
case 2:
callMethod("few");
break;
default:
callMethod("many");
break;
}
Non-trivial fall-through from a branch with statements into one with even more is sometimes the best solution but it's also easy to get wrong, miss, or misunderstand.
Having the less common case opt-out (not opt-in) makes it error prone to the point where linters usually issue a warning on non-trivial fall-through (i.e. after a non-empty branch) and some action (like adding a comment) is required to silence it.
Then, the common case is verbose because of the break
and the rare case even more so because of the comment.
Fall-through as default didn't stand the test of time
In my opinion, fall-through is one of those cases where Java's default didn't stand the test of time.
▚Arrow
Since Java 14, switch
allows using the lambda arrow to "map" from case to code:
switch (number) {
case 1 -> callMethod("one");
case 2 -> callMethod("two");
default -> callMethod("many");
}
It doesn't fall through - not only not by default but not at all, which is superb. And it comes with two bonuses:
-
It's common to write lambdas on a single line and so it's common to have case and branch on a single line, too. (Nothing stopped us from doing the same in the colon form, of course, we just usually didn't.)
-
To have multiple statements in a branch, you need to create a block with
{ }
, which immediately gives you a new scope for local variables, so you can easily reuse variable names in different branches.// This is not how you'd write // this code in real life, but // it demonstrates that the // name `str` can be reused. switch (number) { case 1 -> { var str = "one"; callMethod(str); } case 2 -> { var str = "two"; callMethod(str); } default -> { var str = "many"; callMethod(str); } }
(Again, we could've done the same with colons, but we rarely did.)
▚Colon vs Arrow
To me, this decision is super easy: Arrow form all day, every day.
Arrow form all day, every day
That is, unless I can't avoid fall-through, which I try because it's harder to understand. Fortunately, the ability to list multiple case labels (see below) eliminates a big use case for it.
That leaves non-trivial fall-through as the only reason I see to use the "classic" form and I hope that spotting colons in switch
will become a strong indicator that there's fall-through ahead, which would alert us to pay extra attention to this more complicated construct.
From here on out, I'll only use arrows but all examples also work with :
and break
.
▚Statement vs Expression
▚Switch Statement
This is the "classic" form: The value of the switch variable determines the branch, which then gets executed. The end.
switch (number) {
case 1 -> callMethod("one");
case 2 -> callMethod("two");
default -> callMethod("many");
}
▚Switch Expression
Using switch
as an expression works the same way, but the story doesn't end after the execution.
Instead, the switch as a whole takes on the value of the computation, which can then be assigned to a variable (or passed as an argument, but that's horribly unreadable):
var string = switch (number) {
case 1 -> "one";
case 2 -> "two";
default -> "many";
};
callMethod(string);
▚Exhaustiveness
By definition, an expression has a value and so a switch expression must always compute to one. Consequently there must be a branch for each possible value of the switch variable - this is called exhaustiveness.
A switch expression must cover all possible values. This is called exhaustiveness .
In the example above, without the default branch, string
would be undefined if number
were neither 1
nor 2
, which would make the switch non-exhaustive.
The compiler catches that and throws an error.
But exhaustiveness checks don't end there! While a default branch will always make a switch exhaustive, it isn't required - if the cases cover all possible values, e.g. of an enum, that suffices:
enum Count { ONE, TWO, MANY }
Count count = // ...
var string = switch (count) {
case ONE -> "one";
case TWO -> "two";
case MANY -> "many";
// no default branch needed
};
Without a default branch, new Count
values (say THREE
is added), lead to compile errors, which will make us consider how to handle that new case.
With a default branch, on the other hand, new cases are (silently) caught and processed by it.
Java's switch
allows us to pick the behavior that best fits each given situation.
(NB: Check the section on patterns for more on exhaustiveness.)
▚Statement vs Expression
Some problems can only be solved reasonably with a switch statement. For example, when each case requires calling different methods that have no return values (or they're not needed):
switch (number) {
case 1 -> callOne();
case 2 -> callTwo();
default -> callMany();
}
For other problems, switch expressions are clearly the better fit. For example, when a value needs to be "translated" to a different value:
var string = switch (number) {
case 1 -> "one";
case 2 -> "two";
default -> "many";
};
But there'll be a lot of cases, where it's not clear cut and both approaches work reasonably well. This will often be the case when a value needs to be translated and then passed to a method (or methods) that's the same in each branch:
// translate `number`, then `callMethod` with it
// as switch statement
switch (number) {
case 1 -> callMethod("one");
case 2 -> callMethod("two");
default -> callMethod("many");
}
// as switch expression
var string = switch (number) {
case 1 -> "one";
case 2 -> "two";
default -> "many";
};
callMethod(string);
This is probably a matter of personal taste, but I lean towards using expressions in these scenarios for a few minor reasons. In order or decreasing importance:
When statement and expression work, I lean towards expression
- the expression is checked for exhaustiveness
- the "translate, then call" logic is more directly mirrored on the code, making it a bit easier to spot
- it introduces an additional variable that I can give a name (hopefully a better one than
string
😬), which helps readability
Regarding exhaustiveness, I tend to avoid default branches whenever possible, preferring to get compile errors when things change.
My recommendation when getting to know switch expressions is to frequently implement both variants (it usually only takes a few minutes) and compare them side by side to figure out which one works better in that scenario and why. Such comparisons make great topics for pair programming, code reviews, at the water cooler, and every other bikeshed-adjacent location. In my experience, intuition for what to do when builds after a few weeks of consistent use and reflection.
▚Labels vs Patterns
▚Labels
Not much to say about classic case labels except that you can now have many of them after one case
:
var string = switch (number) {
case 1, 2 -> "few";
default -> "many";
};
Super handy to replace trivial fall-through.
▚Patterns
The details of pattern matching in switch
are still in flux (there'll be a third preview in Java 19), so this section is somewhat speculative, but there are three aspects that are particularly interesting for this conversation.
▚Exhaustiveness
Earlier, I motivated the need for switch expressions to be exhaustive with the fact that an expression has to have a value. But while classic switch statements don't have to be exhaustive, it's surely helpful if they are because then new cases don't accidentally result in no behavior. And "all switches must be exhaustive" is a simpler model than "all switch expressions must be exhaustive".
To be able to get there in the future, it's helpful not to take one more step into the wrong direction in the present and so pattern switches will likely have to be exhaustive - even if used in a statement. That would leave us with "all switches must be exhaustive, except statements with labels" - not very intuitive, but hopefully temporary.
Pattern switches (even as statements) must be exhaustive
Object obj = // ...
switch (obj) {
case String str -> callMethod(str);
// even though this is a statement, it will
// probably have to be exhaustive, in which
// case this default branch (or a total
// pattern) would be needed
default -> { }
}
▚Type Patterns
At the time of writing, Java only supports type patterns, with deconstruction patterns for records proposed by JEP 405.
They can already be used in if
-statements but soon also in switch
, which begs the question when to use what.
Object obj = // ...
// works since Java 16
if (obj instanceof String str)
callStringMethod(str);
else if (obj instanceof Number no)
callNumberMethod(no);
else
callObjectMethod(obj);
// works (as preview) in JDK 17+
switch (obj) {
case String str -> callStringMethod(str);
case Number no -> callNumberMethod(no);
default -> callObjectMethod(obj);
}
I think the switch comes out ahead:
- it more clearly expresses the intend to execute exactly one branch based on
obj
's properties - the compiler checks exhaustiveness
- if a value needs to be computed (not the case here), use as an expression is more succinct
This is a categorically new aspect in our deliberations.
So far we've discussed what kind of switch to use in "switchy" situations but haven't considered that more situations may become "switchy" - this scenario changes that.
It suggests that there are situations where switch
can (should?) replace if
-else
-if
chains.
Let's see another, less immediate example.
There are situations whereswitch
can replaceif
▚When Clauses
When clauses (formerly guarded patterns) refine a pattern with additional boolean checks. While this is currently not being proposed, there has been talk on the mailing list (couldn't find the link 😔) about one day allowing conditions without the preceding pattern. It could work like this (syntax made up by me):
String str = // ...
String length = switch (str) {
case str.length() > 42 -> "long";
case str.length() > 19 -> "medium";
case str.length() > 1 -> "small";
case null || str.length() == 0 -> "empty";
};
Again, this could be an if
-else
-if
chain instead, but again I think the switch comes out ahead (for the same reasons as above).
With switch
becoming more powerful, my guess is that it will start to eat into the use cases for longer if
-else
-if
chains.
And it makes sense because that's the core tenet of switch
:
Here's a bunch of possibilities for this value - pick one and compute.
It communicates that much more clearly than an if
-else
-if
chain and so I hope to some day see it being used in all such situations.
▚Labels vs Patterns
After that excursion into switch
vs if
, let's get back to when to use what form of switch
.
Now: labels vs patterns.
The answer to that is super simple, though, as it is fully determined by what you want to check for the switch variable.
Labels vs patterns is fully determined by what you want to check
Need to compare to specific values? Use labels.
// works in Java 14+
String str = // ...
var number = switch (str) {
case "one" -> 1;
case "two" -> 2;
case "one MILLION" -> 1_000_000;
default -> 0;
};
Need to check structural properties? Use patterns.
// not even proposed and syntax made up by me;
// I picked this very hypothetical example
// because it also switches on a string
String str = // ...
String length = switch (str) {
case str.length() > 42 -> "long";
case str.length() > 19 -> "medium";
case str.length() > 1 -> "small";
case null || str.length() == 0 -> "empty";
};
▚Reflection
How to best use switch
:
- Colons or arrows:
- Always arrows (to avoid dealing with fall-through), except when non-trivial fall-through is needed.
- Statement or expression:
- Often dictated by the problem, but where both work, lean towards expression (to benefit from exhaustiveness checks and to make code clearer by surfacing the logical flow). Initially, consider implementing both variants to build an understanding of the trade-offs.
- Labels or patterns:
- Dictated by the problem, but keep in mind that patterns (particularly "pure" when clauses if they ever come) may make
switch
preferable toif
.