☕ Java

Switch Expressions

Switch expressions, finalized in Java 14 (after preview in Java 12–13), let a switch produce a value directly as an expression, using a new arrow (case L ->) syntax that eliminates fall-through by default, supports multiple case labels in a single case, and is checked by the compiler for exhaustiveness when switching over an enum or (later) a sealed type — addressing fall-through bugs and the awkwardness of assigning a switch's result to a variable that have affected the traditional colon-based switch statement since Java 1.0. This entry covers exactly what was wrong with the old switch statement, the new arrow syntax and multi-label cases, the yield keyword for multi-statement case blocks, and exhaustiveness checking and why it matters for enums and sealed types.

Motivation — Fall-Through Bugs and the Statement/Expression Gap

The traditional switch statement, unchanged in its core behavior since Java 1.0, executes the matching case's code and then continues executing every subsequent case's code as well, unless an explicit break statement is reached — a design directly inherited from C, where it was intentional and occasionally useful, but in practice has been one of the most consistently cited sources of accidental bugs in C-family languages, since forgetting a single break causes execution to silently "fall through" into the next case's code, often producing wrong behavior with no compiler warning at all (modern IDEs and static analyzers added fall-through warnings precisely because the language itself doesn't catch this). A second, separate problem was that switch was purely a statement, never an expression, meaning it couldn't be used directly on the right-hand side of an assignment — producing a value from a switch required declaring a variable beforehand, then assigning to it from inside each case block (each of which also needed its own break to avoid fall-through into the next case's assignment, compounding the fall-through risk), a pattern that was both verbose and exactly the kind of repetitive, error-prone boilerplate that a direct expression form should eliminate. Java 14 (JEP 361, after preview in 12 and 13) introduced switch expressions: a switch can now appear directly as an expression producing a value, using a new arrow syntax (case label -> result) that has no fall-through behavior at all — each case is independent, executes only its own code, and does not flow into the next case under any circumstances, removing the entire fall-through bug category by construction rather than by convention or warning.
Java
// ── Old switch statement — fall-through risk, separate variable assignment
String dayTypeOld;
switch (day) {
    case MONDAY:
    case TUESDAY:
    case WEDNESDAY:
    case THURSDAY:
    case FRIDAY:
        dayTypeOld = "Weekday";
        break;            // forgetting this causes silent fall-through into next case!
    case SATURDAY:
    case SUNDAY:
        dayTypeOld = "Weekend";
        break;
    default:
        throw new IllegalArgumentException("Unknown day: " + day);
}

// ── Java 14+ — switch expression, arrow syntax, no fall-through possible
String dayType = switch (day) {
    case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Weekday";   // multiple labels, one arm
    case SATURDAY, SUNDAY -> "Weekend";
    // no "break" needed or even allowed in arrow form — each arm is independent
};
// Directly assignable as an expression — no pre-declared mutable variable needed,
// and the entire fall-through bug category is structurally impossible here

Arrow Syntax, Multi-Statement Blocks, and yield

In the simplest arrow form, case label -> expression; directly yields that expression's value as the switch expression's overall result for that matching case, with no break and no possibility of falling into another case. Multiple case labels can share a single arm by separating them with commas (case A, B, C -> ...), directly replacing the old pattern of stacking several colon-labeled cases with no code between them purely to share one block of logic — itself a frequent visual source of confusion in the old syntax, since it was easy to misread stacked empty cases as accidental fall-through rather than intentional sharing. When a case's logic genuinely requires more than a single expression — multiple statements, a loop, intermediate local variables — the arrow form also accepts a full block in braces, and within that block the yield keyword (rather than return, which would exit the entire enclosing method, not just the switch expression) produces the value for that case. yield is a contextual keyword, meaning it is only treated specially inside a switch expression's block and remains a perfectly legal identifier (variable or method name) everywhere else in the language, preserving backward compatibility with any existing code that happened to use "yield" as a name. The traditional colon syntax (case label: statements) still exists and can still be used inside a switch expression, in which case fall-through behavior is preserved exactly as in the old statement form (for backward compatibility and for cases where fall-through is genuinely intended) and yield is used to produce the expression's value instead of an assignment — but mixing colon-style fall-through cases with arrow-style cases in the same switch is not allowed; a given switch expression must consistently use one style or the other.
Java
// ── Single expression per arm — most common form ───────────────────────
int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY -> 7;
    case THURSDAY, SATURDAY -> 8;
    case WEDNESDAY -> 9;
};

// ── Block form with yield — for multi-statement case logic ─────────────
int score = switch (grade) {
    case 'A' -> 4;
    case 'B' -> {
        System.out.println("Good job");   // multiple statements allowed in block form
        int bonus = computeBonus();
        yield 3 + bonus;                   // "yield" produces the value — "return" would
                                             // exit the enclosing METHOD, not just the switch
    }
    case 'C' -> 2;
    default -> {
        System.out.println("Needs improvement");
        yield 0;
    }
};

// ── "yield" is contextual — still a legal identifier elsewhere ─────────
int yield = 5;            // perfectly legal outside a switch expression block
System.out.println(yield);

// ── Old colon-style is still usable inside switch expressions (fall-through preserved)
int result = switch (input) {
    case 1:
    case 2:
        yield 100;          // colon-style uses yield too, but DOES fall through between cases
    default:
        yield 0;
};
// Cannot mix colon-style and arrow-style cases within the SAME switch expression

Exhaustiveness Checking — Enums and Sealed Types

A switch expression must be exhaustive: the compiler requires that every possible value of the switch's selector be covered by some case, because a switch expression must always produce a value, and there's no sensible fallback value the compiler could invent for an uncovered case at runtime. For most types this is satisfied simply by including a default case, which catches anything not explicitly listed. For enum types specifically, the compiler can perform genuine exhaustiveness checking without a default: if every single enum constant has its own case, the switch is recognized as exhaustive on those grounds alone, and default may be omitted entirely — and conversely, if a new constant is later added to the enum and the switch expression is not updated to handle it, the code fails to compile with a clear "not exhaustive" error at the next build, rather than silently doing nothing or hitting an unhandled case only at runtime, which is exactly the kind of fail-fast safety net that's valuable when an enum's set of values might grow over time. This same exhaustiveness checking was extended, alongside pattern matching for switch, to sealed types: because a sealed interface or class declares its complete, fixed set of permitted direct subtypes (covered separately under pattern matching), the compiler can likewise verify that a switch over a sealed type's permitted subtypes is exhaustive without a default, and will similarly reject the code at compile time if a new permitted subtype is added later without the switch being updated to handle it — extending the same enum-style safety guarantee to a much broader category of fixed, closed type hierarchies. If a switch expression's selector type does not allow this kind of compiler-verified exhaustiveness (an arbitrary int, String, or any non-sealed, non-enum reference type), a default case becomes mandatory, and omitting it is a compile-time error — the compiler simply has no way to verify every possible input value is covered, so it requires the catch-all explicitly.
Java
// ── Enum exhaustiveness — compiler-verified, no default needed ─────────
enum TrafficLight { RED, YELLOW, GREEN }

String action = switch (light) {
    case RED -> "Stop";
    case YELLOW -> "Caution";
    case GREEN -> "Go";
    // no default needed — compiler verifies all 3 enum constants are covered
};

// If TrafficLight later gains a new constant, e.g. FLASHING_RED, this switch
// expression FAILS TO COMPILE until updated — caught at build time, not runtime:
// error: the switch expression does not cover all possible input values

// ── Non-enum, non-sealed selector — default is mandatory ───────────────
int code = 404;
String meaning = switch (code) {
    case 200 -> "OK";
    case 404 -> "Not Found";
    case 500 -> "Server Error";
    default -> "Unknown";   // REQUIRED — compiler cannot verify exhaustiveness over all ints
};
// Removing "default" here is a COMPILE ERROR:
// "the switch expression does not cover all possible input values"

// ── Sealed type exhaustiveness — same compiler guarantee, broader scope ─
sealed interface Shape permits Circle, Square, Triangle {}
record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}
record Triangle(double base, double height) implements Shape {}

double area = switch (shape) {
    case Circle c -> Math.PI * c.radius() * c.radius();
    case Square s -> s.side() * s.side();
    case Triangle t -> 0.5 * t.base() * t.height();
    // no default needed — Circle/Square/Triangle are the sealed interface's
    // COMPLETE permitted set, verified exhaustive by the compiler
};
// Adding a new permitted subtype to Shape without updating this switch
// fails to compile — the same fail-fast safety enum exhaustiveness provides

Related Topics in Java 9 to 21 Features

Module System (JPMS)
The Java Platform Module System (JPMS), introduced in Java 9 under Project Jigsaw, adds a module as a new, higher-level unit of code organization above the package, allowing a JAR-like artifact to explicitly declare which packages it exports for use by other modules, which modules it depends on, and which packages it keeps entirely internal and inaccessible from outside — closing a long-standing gap in the classpath model where every public class in every JAR on the classpath was accessible to every other JAR, regardless of whether that access was intended. JPMS was also the mechanism used to decompose the JDK itself, which had grown into one monolithic rt.jar, into roughly 90 separate modules, enabling smaller custom runtime images and explicit internal-API encapsulation (notably restricting access to sun.* and other internal packages that many libraries had relied on despite warnings). This entry covers the classpath problems JPMS solves, module-info.java syntax and the requires/exports/opens directives, the distinction between the classpath and module path including automatic and unnamed modules, and jlink custom runtime images.
JShell
JShell, introduced in Java 9, is an interactive Read-Eval-Print Loop (REPL) bundled directly with the JDK, allowing Java statements, expressions, method, class, and variable declarations to be entered and evaluated one at a time at an interactive prompt, with immediate feedback — without requiring a class wrapper, a main method, explicit compilation, or a build tool, all of which Java had always required for even the smallest snippet of executable code before Java 9. This solved a long-standing friction point for learning, quick experimentation, and API exploration, where languages like Python or Ruby offered an immediate interactive prompt but Java required the full edit-compile-run cycle even to test a single line. This entry covers what problem JShell solves and how it differs from a script, core REPL usage including implicit variable creation and forward references, snippet management commands, and how to feed JShell external classes and files.
Collection Factory Methods
Collection factory methods — List.of(...), Set.of(...), and Map.of(...)/Map.ofEntries(...) — introduced in Java 9, provide a concise, standard way to create small, fixed-content, genuinely immutable collections in a single expression, replacing the previously common but verbose combination of Arrays.asList(...), Collections.unmodifiableList(...), and manual add() calls, none of which produced a collection that was both concise to write and fully immutable. This entry covers exactly what problem these factories solve compared to the older idioms, the specific immutability and null-rejection guarantees they provide (which are stricter than Collections.unmodifiableXxx), their behavior around duplicate keys/elements, and important caveats around mutability expectations and performance for very small collections.
Private Interface Methods
Private interface methods, introduced in Java 9, allow an interface to declare methods marked private — with a full method body — that are accessible only from within that same interface (from its default methods, its other private methods, and its private static methods), but completely invisible to implementing classes and to any code outside the interface. They close a code-duplication gap left after Java 8's default methods: when an interface has multiple default methods that share common logic, that shared logic previously either had to be duplicated in each default method or exposed as a public default method purely to enable reuse, polluting the interface's actual API surface with a method meant only for internal reuse. This entry covers the specific problem they solve, the distinction between private instance and private static interface methods, the rules governing what they can call and be called from, and how they interact with the broader default-method design Java established in version 8.