We’re trying to make switch
es simpler to understand at a glance. Misunderstanding the control flow of a switch
is a common source of bugs.
As part of this simplification, new-style arrow (->
) switches are encouraged instead of old-style colon (:
) switches. And where possible, neighboring cases are grouped together (e.g. case A, B, C
).
:
) switch
es:case
and the case
’s code. For example, case HEARTS:
switch
block is large, just skimming each case
can be toilsome. Fall-through can also be conditional (see example 5. below). In this scenario, one would potentially need to reason about all possible flows for each case
. When conditionally falling-through multiple case
s, the number of potential control flows can grow rapidlycase
s are propagated down to later case
s, however the values that initialize those local variables do not propagate in the same way->
) switch
es:case
and the case
’s code. For example, case HEARTS ->
case
s fall through; no control flow analysis neededcase
s (within a switch
)case
s; if you define a local variable within a case
, it can only be used within that specific case
.enum Suit {HEARTS, CLUBS, SPADES, DIAMONDS}; private void foo(Suit suit) { switch(suit) { case HEARTS: System.out.println("Red hearts"); break; case DIAMONDS: System.out.println("Red diamonds"); break; case SPADES: // Fall through case CLUBS: bar(); System.out.println("Black suit"); } }
Which can be simplified by grouping and using a new-style switch:
enum Suit {HEARTS, CLUBS, SPADES, DIAMONDS}; private void foo(Suit suit) { switch(suit) { case HEARTS -> System.out.println("Red hearts"); case DIAMONDS -> System.out.println("Red diamonds"); case SPADES, CLUBS -> { bar(); System.out.println("Black suit"); } } }
return switch ...
Sometimes switch
is used with a return
for each case
, like this:
enum SideOfCoin {OBVERSE, REVERSE}; private String renderName(SideOfCoin sideOfCoin) { switch(sideOfCoin) { case OBVERSE: return "Heads"; case REVERSE: return "Tails"; } // This should never happen, but removing this will cause a compile-time error throw new RuntimeException("Unknown side of coin"); }
Note that even though a case
is present for each possible value of the enum
, a boilerplate “should never happen” clause is still needed. The transformed code is simpler and doesn’t need a “should never happen” clause.
enum SideOfCoin {OBVERSE, REVERSE}; private String renderName(SideOfCoin sideOfCoin) { return switch(sideOfCoin) { case OBVERSE -> "Heads"; case REVERSE -> "Tails"; }; }
If you nevertheless wish to define an explicit “should never happen” clause, this can be accomplished by placing the logic inside a default
case. For example:
enum SideOfCoin {OBVERSE, REVERSE}; private String foo(SideOfCoin sideOfCoin) { return switch(sideOfCoin) { case OBVERSE -> "Heads"; case REVERSE -> "Tails"; default -> throw new RuntimeException("Unknown side of coin"); // should never happen }; }
When the checker detects an existing default
that appears to be redundant, it may suggest a secondary auto-fix which removes the redundant default
and its code (if any).
switch
If every branch of a switch
is making an assignment to the same variable, the code can be simplified into a combined assignment and switch
:
enum Suit {HEARTS, CLUBS, SPADES, DIAMONDS}; int score = 0; private void updateScore(Suit suit) { switch(suit) { case HEARTS: // Fall thru case DIAMONDS: score += -1; break; case SPADES: score += 2; break; case CLUBS: score += 3; } }
This can be simplified as follows:
enum Suit {HEARTS, CLUBS, SPADES, DIAMONDS}; int score = 0; private void updateScore(Suit suit) { score += switch(suit) { case HEARTS, DIAMONDS -> -1; case SPADES -> 2; case CLUBS -> 3; }; }
Taking this one step further: if a local variable is defined, and then immediately followed by a switch
in which every case
assigns to that same variable, then all three (the switch
, the variable declaration, and the assignment) can be merged:
enum Suit {HEARTS, CLUBS, SPADES, DIAMONDS}; private void updateStatus(Suit suit) { int score; switch(suit) { case HEARTS: // Fall thru case DIAMONDS: score = 1; break; case SPADES: score = 2; break; case CLUBS: score = 3; } ... }
Becomes:
enum Suit {HEARTS, CLUBS, SPADES, DIAMONDS}; private void updateStatus(Suit suit) { int score = switch(suit) { case HEARTS, DIAMONDS -> 1; case SPADES -> 2; case CLUBS -> 3; }; ... }
switch
Even when the simplifications discussed above are not applicable, conversion to new arrow switch
es can be automated by this checker:
enum Suit {HEARTS, CLUBS, SPADES, DIAMONDS}; private void processEvent(Suit suit) { switch (suit) { case CLUBS: String message = "hello"; var anotherMessage = "salut"; processMessages(message, anotherMessage); break; case DIAMONDS: anotherMessage = "bonjour"; processMessage(anotherMessage); } }
Note that the local variables referenced in multiple cases are hoisted up out of the switch
statement, and var
declarations are converted to explicit types, resulting in:
enum Suit {HEARTS, CLUBS, SPADES, DIAMONDS}; private void processEvent(Suit suit) { String anotherMessage; switch (suit) { case CLUBS -> { String message = "hello"; anotherMessage = "salut"; processMessages(message, anotherMessage); } case DIAMONDS -> { anotherMessage = "bonjour"; processMessage(anotherMessage); } } }
Here’s an example of a complex statement switch
with conditional fall-through and various control flows. Unfortunately, the checker does not currently have the ability to automatically convert such code to new-style arrow switch
es. Manually converting the code could be a good opportunity to improve its readability.
How many potential execution paths can you spot?
enum Suit {HEARTS, CLUBS, SPADES, DIAMONDS}; private int foo(Suit suit){ switch(suit) { case HEARTS: if (bar()) { break; } // Fall through case CLUBS: if (baz()) { return 1; } else if (baz2()) { throw new AssertionError(...); } // Fall through case SPADES: // Fall through case DIAMONDS: return 0; } return -1; }
Suppress false positives by adding the suppression annotation @SuppressWarnings("StatementSwitchToExpressionSwitch")
to the enclosing element.