Payroll Bands: Fall-Through, a Missing yield and an Unboxing NPE
A payroll utility assigns tax bands. Version A uses a classic switch statement over an int bonus: case 0 assigns band 1 with no break, case 1 assigns band 2 and breaks, default assigns band 3. An employee with bonus 0 is reported in band 2. Version B rewrites the logic as a switch expression with arrow labels and yield; one arm is a braced block whose last statement is a println, and javac rejects the file with "missing yield statement". Version C builds the band from an override map: Integer override = overrides.get(employeeId); int band = override != null ? 0 : override; which compiles but throws NullPointerException for employees with no override entry. Version D switches on a String band name returned by a lookup that may return null, and the switch throws NullPointerException before reaching default. Version E scans a two-dimensional array of daily hours, one row per employee; continue inside the inner loop is meant to skip the rest of that employee's week, but only the inner index advances.
1. Explain the Version A and Version B failures: what rules govern fall-through, break and yield in switch statements versus switch expressions?
Read the worked answer
Version A uses colon labels, which fall through. After case 0: assigns band 1 there is no break, so execution continues into case 1: and assigns band 2, then breaks - a bonus of 0 ends up in band 2. Fall-through is a feature (grouping labels), not an accident, but it must be deliberate. The position of default is irrelevant to matching: it is a label, not a first or last resort, and if it is written first it also falls through into the following cases.
Version B uses a switch expression with arrow labels, which never fall through. Every arm must produce a value, either directly (case 1 -> 2;) or via yield inside a block (case 1 -> { log(); yield 2; }). A braced arm that ends with a println has no yield, so javac reports "missing yield statement". return inside an arm is illegal when the switch expression's value is being produced, and break cannot carry a value. A colon-style switch expression is legal too, but then each non-empty arm needs yield rather than break.
A switch expression over int also must be exhaustive, which for primitives means a default arm is mandatory - omitting it is a compile-time error.
2. Version C compiles cleanly yet throws NullPointerException. Explain the reasoning behind the result type and give two safe rewrites.
Read the worked answer
The conditional operator computes a single result type from both operands. When one operand is a primitive numeric type (0, an int) and the other is a boxed numeric type (Integer), binary numeric promotion applies: the boxed operand is unboxed to int, and the result type is int. The unboxing happens at runtime on whichever operand the condition selects.
With a missing entry, override is null, the condition override != null is false, so the selected operand is override - and Integer.intValue() is invoked on null. The NPE is thrown before the assignment to band ever completes. The compiler cannot help: unboxing is a runtime conversion, and a null flowing into a numeric context is only discovered when the branch actually executes, which is why the unit tests with a populated map passed.
Safe rewrites:
int band = overrides.getOrDefault(employeeId, 0); // no conditional at all
int band2 = Optional.ofNullable(overrides.get(employeeId)).orElse(0);
The getOrDefault form still NPEs if the map actually stores a null value for the key; Optional.ofNullable(...).orElse(0) does not, because the null check happens before the unboxing of the returned value.
3. Version D throws before default runs; Version E only advances the inner index. Explain both and give the correct code for Version E.
Read the worked answer
Version D: a switch on a String (or any reference type) evaluates the selector and invokes, in effect, selector.hashCode() to choose the case. A null selector therefore throws NullPointerException immediately, before any label is compared - default is a label like any other, not a null guard. Java 17 has no case null (that arrived with pattern matching for switch later); the fix is an explicit guard before the switch:
if (name == null) { band = 0; }
else switch (name) { case "A" -> band = 1; default -> band = 0; }
Version E: an unlabeled continue targets the innermost enclosing loop, so it abandons the rest of the current inner iteration and moves to the next inner index; the outer loop continues normally. To abandon the rest of the employee's week you need a label on the outer loop:
outer:
for (int emp = 0; emp < hours.length; emp++) {
for (int day = 0; day < hours[emp].length; day++) {
if (hours[emp][day] < 0) continue outer; // skip to next employee
total += hours[emp][day];
}
}
break outer; would exit both loops entirely. A label used with continue must be attached to a loop statement; break additionally accepts any labeled statement.