Pattern Matching for switch (JEP 406): Type Dispatch in switch (Preview)
Preview Feature in Java 17 (JEP 406 — First Preview). Requires
--enable-preview. This feature evolved through Java 18 (JEP 420), Java 19 (JEP 427), Java 20 (JEP 433), and was finalized in Java 21 (JEP 441). The Java 21 final version has minor syntax differences from this Java 17 preview — documented below.
Enabling Preview Features
Pattern matching for switch requires --enable-preview in Java 17. Add to your build tool:
Maven (pom.xml):
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>17</release>
<compilerArgs><arg>--enable-preview</arg></compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration><argLine>--enable-preview</argLine></configuration>
</plugin>
At runtime: java --enable-preview MyApp
What Problem Does This Solve?
After mastering sealed classes (Article 7) and pattern matching for instanceof (Article 5), the natural next step is dispatching on type in a switch expression. But before JEP 406, switch could not use patterns:
// Java 17 without preview — only works with constants (values, enums, strings)
// Cannot switch on type patterns
So developers used if-else if chains with instanceof:
// Verbose and not exhaustiveness-checked
String describe(Object obj) {
if (obj instanceof Integer i) {
return "Integer: " + i;
} else if (obj instanceof String s) {
return "String of length " + s.length();
} else if (obj instanceof Double d) {
return "Double: " + d;
} else {
return "Unknown";
}
}
JEP 406 lets you write this as a switch expression:
// Java 17 with --enable-preview
String describe(Object obj) {
return switch (obj) {
case Integer i -> "Integer: " + i;
case String s -> "String of length " + s.length();
case Double d -> "Double: " + d;
default -> "Unknown";
};
}
Type Patterns in switch
A type pattern case matches if the selector expression is an instance of the specified type:
static double area(Object shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.5 * t.base() * t.height();
default -> throw new IllegalArgumentException("Not a shape: " + shape);
};
}
The pattern variable (c, r, t) is bound and in scope in the case body.
With Sealed Types: No default Needed
If the selector type is sealed and all permitted subtypes are covered:
sealed interface Shape permits Circle, Rectangle, Triangle {}
static double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.5 * t.base() * t.height();
// No default — compiler knows all cases are covered
};
}
Adding a new subtype to permits causes a compile error in this switch — the exhaustiveness check protects you.
Guarded Patterns
Guarded patterns add a condition after the type check. In Java 17 (JEP 406), the syntax uses &&:
// Java 17 preview — guarded pattern with &&
static String classify(Object obj) {
return switch (obj) {
case Integer i && i < 0 -> "Negative integer: " + i;
case Integer i && i == 0 -> "Zero";
case Integer i -> "Positive integer: " + i;
case String s && s.isEmpty() -> "Empty string";
case String s -> "String: " + s;
default -> "Other: " + obj;
};
}
Important Java 21 difference: In Java 21’s final JEP 441, && is replaced with the when keyword:
// Java 21 final — guarded pattern with when
return switch (obj) {
case Integer i when i < 0 -> "Negative: " + i;
case Integer i when i == 0 -> "Zero";
case Integer i -> "Positive: " + i;
default -> "Other";
};
If you plan to upgrade to Java 21, be aware that && guards compile only with Java 17 --enable-preview — you will need to change them to when during migration.
Null Handling
Traditional switch throws NullPointerException if the selector is null. JEP 406 adds an explicit case null:
// Java 17 preview
static String describeNull(Object obj) {
return switch (obj) {
case null -> "It's null";
case Integer i -> "Integer: " + i;
case String s -> "String: " + s;
default -> "Something else";
};
}
describeNull(null); // "It's null" — no NullPointerException
describeNull(42); // "Integer: 42"
describeNull("hello"); // "String: hello"
case null can be combined with default:
return switch (obj) {
case null, default -> "null or unrecognized";
case Integer i -> "Integer: " + i;
};
Without case null, passing null to a pattern switch still throws NullPointerException, same as a traditional switch.
Ordering of Cases
Pattern cases are evaluated top-down. More specific cases must come before more general ones:
// Correct: specific before general
return switch (obj) {
case Integer i && i > 0 -> "positive"; // specific
case Integer i -> "non-positive"; // general
default -> "not integer";
};
// Compile error: general case before specific case — unreachable
return switch (obj) {
case Integer i -> "any integer"; // dominates the next case
case Integer i && i > 0 -> "positive"; // error: dominated by previous case
default -> "other";
};
The compiler detects dominance: if case A covers every value that case B covers, then B is unreachable and flagged as an error.
Pattern Switch in Statements
Pattern switch also works as a statement (not just an expression):
// Switch statement with patterns
switch (event) {
case UserCreated u -> onboard(u.userId());
case UserDeleted d -> cleanup(d.userId());
case PasswordChanged p -> notifyUser(p.userId());
default -> log.warn("Unhandled event: " + event);
}
Comparing Java 17 Preview vs Java 21 Final
| Feature | Java 17 (JEP 406) Preview | Java 21 (JEP 441) Final |
|---|---|---|
| Type patterns in switch | Yes | Yes |
| Null case | Yes | Yes |
| Guarded patterns | case T t && condition | case T t when condition |
| Record patterns in switch | No | Yes (JEP 440) |
| Exhaustiveness for sealed | Yes | Yes |
No --enable-preview needed | No | Yes (production-ready) |
The main migration change from Java 17 to Java 21 is replacing && guards with when.
Real-World Example: Event Processing
sealed interface AppEvent
permits AppEvent.UserLoggedIn, AppEvent.OrderPlaced, AppEvent.PaymentFailed {}
record UserLoggedIn(String userId, Instant at) implements AppEvent {}
record OrderPlaced(String orderId, double total) implements AppEvent {}
record PaymentFailed(String orderId, String reason) implements AppEvent {}
// Java 17 with --enable-preview
String processEvent(AppEvent event) {
return switch (event) {
case UserLoggedIn u -> "User " + u.userId() + " logged in";
case OrderPlaced o && o.total() > 1000 -> "Large order: " + o.orderId();
case OrderPlaced o -> "Order placed: " + o.orderId();
case PaymentFailed p -> "Payment failed for " + p.orderId() + ": " + p.reason();
};
}
Should You Use This Preview Feature in Java 17?
Reasons to Use It
- Your codebase is already on Java 17 and won’t upgrade to Java 21 soon
- The switch dispatching pattern is pervasive in your code (event processing, domain modeling)
- You accept the migration cost of changing
&&towhenwhen upgrading to Java 21
Reasons to Wait
- You are on Java 17 transitionally and plan to move to Java 21 within 6–12 months
- Your organization prohibits preview features in production
- The
&&→whenmigration is not worth the preview-feature risk
Pragmatic recommendation: Use preview features in new non-critical code. The API stabilized across 4 preview rounds with only the guard syntax changing — the risk is low. But document every use of --enable-preview so you can find and update them when upgrading.
Summary
| Feature | Java 17 Preview Syntax |
|---|---|
| Type pattern | case Integer i -> |
| Guarded pattern | case Integer i && i > 0 -> |
| Null case | case null -> |
| Null + default | case null, default -> |
| No default for sealed | Exhaustiveness checked by compiler |
| Case ordering | Specific before general; compiler detects dominance |
What’s Next
Article 9: Enhanced Pseudo-Random Number Generators (JEP 356) covers the new RandomGenerator interface hierarchy — a modern, pluggable, and algorithm-agnostic API for random number generation in Java 17.