Contents
- Before and After
- Type Patterns
- Guarded Patterns with when
- Null Handling in switch
- Sealed Classes and Exhaustiveness
// BEFORE — instanceof chain if/else
static String format(Object obj) {
if (obj instanceof Integer i) return "int: " + i;
else if (obj instanceof Double d) return "double: " + d;
else if (obj instanceof String s && s.length() > 5) return "long string: " + s;
else if (obj instanceof String s) return "short string: " + s;
else if (obj == null) return "null";
else return "other: " + obj;
}
// AFTER — switch with type patterns
static String format(Object obj) {
return switch (obj) {
case Integer i -> "int: " + i;
case Double d -> "double: " + d;
case String s when s.length() > 5 -> "long string: " + s;
case String s -> "short string: " + s;
case null -> "null";
default -> "other: " + obj;
};
}
Each case label can be a type pattern: a type followed by a binding variable name. The variable is bound when that case is selected.
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double base, double height) implements Shape {}
// Exhaustive switch — no default needed with sealed types
static double area(Shape s) {
return switch (s) {
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();
};
}
// Ordering matters: more specific patterns must come before broader ones
static String describe(Number n) {
return switch (n) {
case Integer i -> "Integer: " + i;
case Long l -> "Long: " + l;
case Double d -> "Double: " + d;
default -> "Other number: " + n;
// case Number x -> ... would be a compile error here: it dominates all above
};
}
A broader type pattern dominates narrower ones. If you place case Number n before case Integer i, the compiler will report a dominance error.
The when clause adds a boolean guard to a type pattern. The binding variable is in scope within the guard expression.
static String classify(Object obj) {
return switch (obj) {
case Integer i when i < 0 -> "negative int";
case Integer i when i == 0 -> "zero";
case Integer i when i % 2 == 0 -> "even positive: " + i;
case Integer i -> "odd positive: " + i;
case String s when s.isBlank() -> "blank string";
case String s -> "string: " + s;
case null -> "null";
default -> "other: " + obj;
};
}
// Record + when guard — very expressive
record Order(String id, double total, String status) {}
static String orderLabel(Object obj) {
return switch (obj) {
case Order o when "PAID".equals(o.status()) && o.total() > 1000
-> "VIP paid: " + o.id();
case Order o when "PAID".equals(o.status())
-> "Paid: " + o.id();
case Order o when "PENDING".equals(o.status()) && o.total() > 500
-> "High-value pending: " + o.id();
case Order o
-> "Order: " + o.id() + " [" + o.status() + "]";
default
-> "Not an order";
};
}
Before Java 21, passing null to a switch always threw NullPointerException. Now you can explicitly handle null with case null:
static void handle(String input) {
switch (input) {
case null -> System.out.println("No input");
case "" -> System.out.println("Empty");
case String s when s.isBlank() -> System.out.println("Blank");
default -> System.out.println("Input: " + input);
}
}
// null and default can be combined
switch (value) {
case null, "UNKNOWN" -> System.out.println("Not available");
case "ACTIVE" -> System.out.println("Active");
default -> System.out.println("Other: " + value);
}
Without a case null, passing null to a switch still throws NullPointerException — the same as before Java 21. You must explicitly opt into null handling.
When the selector is a sealed type, the compiler enforces exhaustiveness — missing a permitted subtype is a compile error, not a runtime surprise.
sealed interface Expr permits Num, Add, Mul, Neg {}
record Num(int value) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Mul(Expr left, Expr right) implements Expr {}
record Neg(Expr expr) implements Expr {}
// No default needed — sealed hierarchy is fully covered
static int eval(Expr expr) {
return switch (expr) {
case Num(int v) -> v;
case Add(var l, var r) -> eval(l) + eval(r);
case Mul(var l, var r) -> eval(l) * eval(r);
case Neg(var e) -> -eval(e);
};
}
// Result<T> — sealed generic interface
sealed interface Result<T> permits Ok, Err {}
record Ok<T>(T value) implements Result<T> {}
record Err<T>(String msg) implements Result<T> {}
static <T> T unwrap(Result<T> result) {
return switch (result) {
case Ok<T>(var v) -> v;
case Err<T>(var msg) -> throw new RuntimeException("Error: " + msg);
};
}
Adding a new permitted subtype to a sealed interface will break all exhaustive switches over that type at compile time — which is intentional and desirable. It forces you to handle every case.