Use instanceof pattern matching, switch pattern matching with guards, and deconstruction patterns.
Published February 26, 2025
Pattern matching eliminates the cast-after-instanceof boilerplate and enables expressive, type-safe switch expressions. Introduced incrementally: instanceof (Java 16), switch (Java 21).
// Old way
if (obj instanceof String) {
String s = (String) obj; // redundant cast
System.out.println(s.length());
}
// New way: binding variable declared inline
if (obj instanceof String s) {
System.out.println(s.length()); // s is String, already cast
}
// Works in complex conditions
if (obj instanceof String s && s.length() > 5) {
System.out.println("Long string: " + s);
}
// Negation
if (!(obj instanceof String s)) {
throw new IllegalArgumentException("Expected String");
}
// s NOT in scope here
public String describe(Object obj) {
return switch (obj) {
case Integer i -> "Integer: " + i;
case Long l -> "Long: " + l;
case Double d -> "Double: " + d;
case String s -> "String of length " + s.length();
case int[] arr -> "int array of length " + arr.length;
case null -> "null";
default -> "Something else: " + obj.getClass().getSimpleName();
};
}
public 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 -> "positive int: " + i;
case String s when s.isBlank() -> "blank string";
case String s -> "string: " + s;
default -> "other";
};
}
record Point(int x, int y) {}
record Line(Point start, Point end) {}
Object shape = new Line(new Point(0, 0), new Point(3, 4));
// Deconstruct nested records
if (shape instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
double length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
System.out.println("Length: " + length);
}
// In switch
String result = switch (shape) {
case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
"Line from (%d,%d) to (%d,%d)".formatted(x1, y1, x2, y2);
case Point(int x, int y) -> "Point at (%d,%d)".formatted(x, y);
default -> "Unknown shape";
};
sealed interface Expr permits Num, Add, Mul {}
record Num(int value) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Mul(Expr left, Expr right) implements Expr {}
public int eval(Expr expr) {
return switch (expr) {
case Num(int v) -> v;
case Add(Expr l, Expr r) -> eval(l) + eval(r);
case Mul(Expr l, Expr r) -> eval(l) * eval(r);
// Exhaustive — no default needed because Expr is sealed
};
}
// eval(new Add(new Num(3), new Mul(new Num(4), new Num(5)))) → 23
sealed interface ApiResult<T> permits ApiResult.Ok, ApiResult.Error {}
record Ok<T>(T data) implements ApiResult<T> {}
record Error<T>(int code, String message) implements ApiResult<T> {}
public ResponseEntity<?> handle(ApiResult<User> result) {
return switch (result) {
case Ok(User user) -> ResponseEntity.ok(user);
case Error(int code, String msg) when code == 404 -> ResponseEntity.notFound().build();
case Error(int code, String msg) -> ResponseEntity.status(code).body(msg);
};
}
instanceof chains and match discriminated unions.when guards (previously && after the type pattern) make switch expressions far more expressive than traditional switch.