Fall-Through in an Order State Machine
An order service models state as enum Status { NEW, PAID, SHIPPED, REFUNDED, CLOSED }. One method uses a colon-form switch: case NEW logs "new" and then falls into case PAID's audit call; case REFUNDED increments refundCount and then falls into case CLOSED's closeCount++ before the break. Nothing fails to compile, so every NEW order writes two audit entries and every refund also increments closeCount, and no test catches it because the counters are only printed nightly. A second method switches over a sealed interface Event permits Created, Paid, Shipped (all records) and the team plans to add a Cancelled record next sprint. A legacy queue occasionally delivers a null status.
1. State exactly what the colon-form switch does for a NEW order and a REFUNDED order, and rewrite it so the bug cannot recur.
Read the worked answer
Colon-form switches fall through unless a break (or return/throw) is reached, and labels with no statements fall through silently. So:
NEWโ executes thelog("new")body and thePAIDaudit body, then breaks.REFUNDEDโ incrementsrefundCount, then falls intoCLOSEDand incrementscloseCounttoo.
Arrow form removes fall-through entirely:
switch (s) {
case NEW -> log("new");
case PAID -> audit("paid");
case SHIPPED -> audit("shipped");
case REFUNDED -> refundCount++;
case CLOSED -> closeCount++;
}
Each label gets its own body; multiple labels can share one with case PAID, SHIPPED -> audit(s);. yield is only for switch expressions, not statements. Note that a plain enum switch statement using constant labels does not have to be exhaustive, which is another reason a missing branch compiles quietly.
2. If `Cancelled` is added to the `permits` clause, what does the compiler do at each switch site, and why is a `default` branch the wrong defensive move?
Read the worked answer
With a sealed hierarchy, the compiler knows the complete set of permitted subtypes. A switch expression (or a pattern switch statement) over Event is exhaustive if every permitted subtype is covered - no default is required, and adding Cancelled makes every such switch non-exhaustive, producing a compile error at exactly the sites that need new handling.
That is the point: the error is the feature. Adding default -> handleUnknown(e) first would silently swallow Cancelled into a generic path, and the compiler would stay quiet. Reserve default for genuinely open types.
Record deconstruction keeps the branches cheap:
String describe(Event e) {
return switch (e) {
case Created(var id, var at) -> "new " + id;
case Paid p -> "paid " + p.amount();
case Shipped s -> "shipped " + s.tracking();
};
}
Ordering matters in pattern switches: put more specific patterns before broader ones, otherwise you get a dominance compile error.
3. What happens when the selector is null in each of the two methods, and how should you handle it?
Read the worked answer
A null selector throws NullPointerException immediately, before any label is tested - this is true for both colon and arrow form, and for enum as well as String selectors. The switch never sees a null branch.
The rule of thumb:
- An enum switch with only constant labels cannot use
case null. Validate at the boundary -Objects.requireNonNull(status, "status")with a message, or map the legacy null to a realStatus.UNKNOWNconstant before it reaches the domain. - A pattern switch may use
case null -> ..., andcase null, default -> ...to combine the two. That is only legal when the switch already uses patterns, so a pure enum switch is not eligible.
Do not catch the NullPointerException around the switch: it also swallows genuine bugs inside the branch bodies.