Evolution of Java's switch: From Java 7 to Java 26
Java turned 30 in 2025. Plenty of production code still runs on Java 6 or 8, and even reasonably modern shops often pin to Java 11 or 17. The current LTS is Java 25 (released September 2025), and the latest stable release is Java 26 (March 2026). Across those versions the humble switch keyword has been quietly overhauled several times: string cases, switch expressions, arrow syntax with direct return values, multi-constant cases, a new yield keyword, and — as of Java 21 — pattern matching as a standard feature. This article walks through each addition in the order it landed.
Java 7
switch with String cases
Before Java 7, switch only accepted integer or char values. Passing a String produced a compile error like this:
test/App.java:27: incompatible types
found : java.lang.String
required: int
switch (url) {
^
1 error
Starting with Java 7, String became a valid switch target:
String str = "thekodelab.com";
switch (str) {
case "thekodelab.com": return "Blog Home";
case "google.com": return "Search Engine";
default: return "UNKNOWN";
}
Under the hood, the compiler still switches on integers. It calls String.hashCode() to convert the argument, then — because hash codes can collide — emits a secondary String.equals(Object) check inside each case body. There’s a good write-up on Stack Overflow: How switch case with string (new in Java 1.7) works internally?.
Incidentally, enum values behave the same way: when you switch on an enum, the compiler calls Enum.ordinal() to get an integer. So a switch argument has always been an integer at the bytecode level (and char is just a small integer too). A consequence is that the String or enum you pass must not be null — the compiler-inserted hashCode() or ordinal() call would throw NullPointerException before any default: label could catch it. These restrictions exist because switch is optimized at compile time, which is also why it’s faster than a chain of if/else at runtime. As of Java 21, a null case is finally part of the language — more on that below.
Java 12 to 14
Java 12 introduced a batch of preview features around switch. Java 13 refined them, and Java 14 promoted them to standard features. (“Preview” means the feature may still change or be removed; “standard” means it’s stable and safe to rely on.)
- JEP 325: Switch Expressions (Preview) — openjdk.org/jeps/325
- JEP 354: Switch Expressions (Second Preview) — openjdk.org/jeps/354
- JEP 361: Switch Expressions — openjdk.org/jeps/361
All the code samples below assume this enum:
public enum EUrl {
HOME, GOOGLE, BING, OTHER;
}
Statements vs. expressions
A quick refresher. A statement is a unit of code that performs an action — declaring a variable, mutating state, branching control flow — without itself producing a value. An expression is a unit of code that evaluates to a value: arithmetic, logical operations, method calls that return something. In int x = 1 + 2 * 3;, the whole line is a statement (it binds x), and the 1 + 2 * 3 on the right-hand side is an expression that evaluates to 7.
switch expressions
Historically, switch was a statement. It took an expression as input — switch(num), switch(1 + 1), switch(getNumber()) — and after evaluating the cases it would execute some control flow without returning a value. You could fake a “return value” by putting return inside each case, but that actually exits the enclosing method; you can’t capture the result and keep working within the same method.
Java 12 introduced switch expressions as a preview, and Java 14 shipped them as a standard feature. Now switch itself can be an expression that evaluates to a value:
String url = switch (eurl) {
case HOME -> "thekodelab.com";
case GOOGLE -> "google.com";
case BING -> "bing.com";
default -> "UNKNOWN";
};
// Note the trailing semicolon
The new case LABEL -> VALUE form returns its right-hand side directly to the variable being assigned. Previously, the only way to do something similar was case LABEL: return VALUE;, which exited the method entirely — forcing you to extract the switch into its own helper method even when you weren’t going to reuse it.
On VS Code with a JDK older than 14, the above code triggers Switch Expressions are supported from Java 14 onwards only Java(1073743545).
The new yield keyword and case blocks
Arrow syntax keeps things terse, but sometimes a case genuinely needs multiple statements before producing its value. For that, wrap the body in braces and use the new yield keyword to produce the result. Here’s the previous example with a logging side effect and a null check in the default branch:
String url = switch (eurl) {
case HOME -> "thekodelab.com";
case GOOGLE -> "google.com";
case BING -> "bing.com";
default -> {
System.err.println("Unknown type");
if (defaultUrl != null) {
yield defaultUrl;
} else {
yield "UNKNOWN";
}
}
};
Java 12’s JEP 325 originally used break VALUE; for this, but JEP 354 in Java 13 replaced it with yield VALUE; because break can also be followed by a label, and the overlap was confusing. See What does the new keyword “yield” mean in Java 13? for the details. yield became a standard feature in Java 14.
Multi-constant cases
This one lets a single case list several values — case 1, 2, 3: or case 1, 2, 3 ->. Preview in Java 12, standard in Java 14:
String type = switch (eurl) {
case HOME -> "My Blog https://thekodelab.com/";
case GOOGLE, BING -> "Search Engine";
default -> {
System.err.println("Unknown type");
yield "UNKNOWN";
}
};
On VS Code with a JDK older than 14, you’ll see Multi-constant case labels supported from Java 14 onwards only Java(1073743543).
Some articles call this “multi-value case,” but that phrasing suggests the cases could hold variables. They can’t — allowing variables would defeat the compile-time optimizations. “Multi-constant” is the accurate name.
Why the constraint? In ahead-of-time compiled languages like Java, switch is faster than if/else precisely because the case labels have to be known at compile time. If your branch condition depends on a variable, you have no choice but to use if/else. The compiler also inspects how densely packed your case values are and picks between two different bytecode forms — tableswitch (for dense values) and lookupswitch (for sparse ones). The dense form is measurably faster. Another consequence of this is that switch cases have to be integral constants, which is a whole separate discussion.
Java 17 to 20: four previews of pattern matching
Java 17 is an LTS release, and it’s also where switch started gaining pattern matching — though still as a preview. In fact, every release from Java 17 through Java 20 shipped another preview iteration of the same feature, each tweaking the syntax a bit. Everything in this section was still a preview during that window; all of it graduated to a standard feature in Java 21 under JEP 441, which has its own section below.
null cases (preview)
As mentioned earlier, switch is compiled down to integer comparisons, which is why null was never a valid input. This preview adds explicit support for a null case. It’s essentially syntactic sugar — the compiler inserts the null check for you so you can write it as just another case inside the block:
EUrl eurl = null;
String type = switch (eurl) {
case null -> {
System.err.println("It's null!");
yield "UNKNOWN";
}
case HOME -> "My Blog https://thekodelab.com/";
case GOOGLE, BING -> "Search Engine";
default -> {
System.err.println("Unknown type");
yield "UNKNOWN";
}
};
Pattern matching (preview)
To understand this one, start with a standard feature that shipped in Java 16: JEP 394: Pattern Matching for instanceof. It lets you combine a type check with a typed binding in a single expression:
// Old code
if (obj instanceof Integer) {
Integer num = (Integer) obj;
System.out.printf("%d", num);
} else if (obj instanceof Double) {
Double num = (Double) obj;
System.out.printf("%f", num);
}
// Java 16 pattern matching
if (obj instanceof Integer num) {
System.out.printf("%d", num);
} else if (obj instanceof Double num) {
System.out.printf("%f", num);
}
The new form is noticeably shorter, so of course switch wants the same trick. Pattern matching for switch was previewed four times in a row, and during the preview period it looked like this:
String str = switch (obj) {
case Integer n -> String.format("int %d", n);
case Double n -> String.format("double %f", n);
default -> obj.toString();
};
Anyone who’s written code that branches on runtime type will appreciate how much cleaner this reads compared to a cascade of instanceof checks.
The new when keyword (preview)
More syntactic sugar. If your matched case needs an additional if guard, nesting an if inside a case block gets deeply indented fast. The when keyword lets you attach a guard directly to the case label:
// Without `when`
String str = switch (obj) {
case Integer n -> {
if (n >= 100) {
yield String.format("int %06d", n);
} else {
yield String.format("int %03d", n);
}
}
default -> obj.toString();
};
// With `when`
String str = switch (obj) {
case Integer n when n >= 100 -> String.format("int %06d", n);
case Integer n -> String.format("int %03d", n);
default -> obj.toString();
};
One awkward corner: if you want multiple when guards for the same type, you have to repeat Integer n on each case — a constraint that carried over into the Java 21 standard form as well.
The four preview JEPs
If you want the full details, the OpenJDK JEP pages are the authoritative source. Here are the four consecutive previews of pattern matching for switch, one per release:
- Java 17 — JEP 406: Pattern Matching for switch (Preview) — openjdk.org/jeps/406
- Java 18 — JEP 420: Pattern Matching for switch (Second Preview) — openjdk.org/jeps/420
- Java 19 — JEP 427: Pattern Matching for switch (Third Preview) — openjdk.org/jeps/427
- Java 20 — JEP 433: Pattern Matching for switch (Fourth Preview) — openjdk.org/jeps/433
Java 21: pattern matching ships
Java 21 (September 2023) is the LTS that followed Java 17, and it’s one of the most significant milestones in the history of switch. The four consecutive previews from Java 17 through 20 all graduated at once under JEP 441: Pattern Matching for switch — null cases, type patterns, when guards, the whole set. It’s all production-ready.
Java 21 also added something genuinely useful on top: exhaustiveness checks for sealed interfaces and enum types. If the switch selector is a sealed interface, the compiler checks that every permitted subtype has a matching case — missing one is a compile error. That promotes switch into a proper type-safe dispatch mechanism.
Here’s an example that combines null cases, type patterns, when guards, and sealed exhaustiveness:
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 {}
static double area(Shape shape) {
return switch (shape) {
case null -> 0;
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s when s.side() > 0 -> s.side() * s.side();
case Square s -> 0;
case Triangle t -> t.base() * t.height() / 2;
};
}
In the Java 21 standard form, null and default can also be combined as case null, default ->, which is cleaner when you don’t want special handling for null. And the sealed exhaustiveness check means that the day someone adds a new subtype to Shape, every switch that hasn’t been updated will fail to compile — which saves a surprising number of “forgot to handle the new type” bugs.
Java 23 to 26: Primitive Types in Patterns (still in preview)
After Java 21 the core switch syntax has been stable, but the pattern matching ecosystem kept expanding. Starting in Java 23, OpenJDK began previewing another switch-centric feature: Primitive Types in Patterns, instanceof, and switch. It lets case labels do pattern matching against primitive types directly, and it extends the switch selector to accept long, float, double, and boolean — no longer restricted to the integer family.
The feature first previewed in Java 23, then carried through Java 24, the Java 25 LTS, and Java 26 — four previews and counting, with no finalization date yet announced. Here’s what it looks like:
// Switch selector can be long/float/double/boolean
long code = 42L;
String size = switch (code) {
case 0L -> "zero";
case 1L, 2L, 3L -> "small";
default -> "other";
};
// case can use primitive-type patterns, combined with `when` guards
// and the new automatic conversion rules
Object obj = 42;
String label = switch (obj) {
case int i when i >= 0 -> "non-negative int " + i;
case int i -> "negative int " + i;
case long l -> "long " + l;
case double d -> "double " + d;
default -> "other";
};
The interesting concept underneath this preview is “unconditional exactness”: if the conversion between two types loses no information (e.g. int can always widen to double, but the reverse may lose precision), the compiler allows the pattern match implicitly; otherwise it asks the developer to express intent explicitly. Each preview round has been tuning these rules so that switch behaves consistently with the rest of the language. Java 26’s JEP 530 tightened the dominance checks another notch.
- Java 23 — JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview) — openjdk.org/jeps/455
- Java 24 — JEP 488 (Second Preview) — openjdk.org/jeps/488
- Java 25 — JEP 507 (Third Preview) — openjdk.org/jeps/507
- Java 26 — JEP 530 (Fourth Preview) — openjdk.org/jeps/530
Wrapping up
From Java 7’s String cases, to Java 14’s switch expressions and yield, to Java 21’s standard pattern matching, all the way to Java 26’s still-evolving Primitive Types in Patterns — switch has gone from a barebones conditional branch to a type-safe, highly readable dispatch mechanism at the heart of modern Java. If your codebase is still on Java 8 or 11, upgrading to Java 21 or the current Java 25 LTS unlocks switch rewrites that are both more concise and more robust. It’s worth spending a weekend digging out the old switch/case blocks and refactoring them.