1. Introduction
Pattern Matching in Java is the ability to test an object against a pattern and extract data from it in a safe, concise way. Instead of using instanceof
checks followed by casting, pattern matching lets you combine these operations.
2. Pattern Matching for instanceof
Before Java 16:
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.toUpperCase());
}
With Pattern Matching
if (obj instanceof String s) {
System.out.println(s.toUpperCase());
}
Key points:
- Eliminates boilerplate casting.
- The variable
s
is only available inside theif
block. - Works with flow scoping: if the compiler knows
obj
is aString
, you can use it.
3. Pattern Matching for switch
Traditional switch
:
static String format(Object obj) {
return switch (obj) {
case Integer i -> "int " + i;
case Long l -> "long " + l;
case String s -> "String " + s.toUpperCase();
default -> obj.toString();
};
}
Features:
- No need for explicit casting.
- Works with sealed classes (see next section).
- Can match null safely with
case null
.
4. Record Patterns
Records are great for data carriers. Record patterns let you destructure them inside switch
or if
.
record Point(int x, int y) {}
static String printPoint(Object obj) {
return switch (obj) {
case Point(int x, int y) -> "Point at (" + x + ", " + y + ")";
default -> "Not a point";
};
}
Nested record patterns:
record Rectangle(Point topLeft, Point bottomRight) {}
static void printRectangle(Rectangle r) {
if (r instanceof Rectangle(Point(int x1, int y1), Point(int x2, int y2))) {
System.out.println("Rectangle from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")");
}
}
5. Sealed Types with Pattern Matching
Sealed classes let you control subclassing. Pattern matching works perfectly with them.
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
static double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
};
}
Because Shape
is sealed, the compiler checks exhaustiveness — no default
needed.
6. Guarded Patterns
Sometimes you need an extra condition on a case.
static String typeOfNumber(Number n) {
return switch (n) {
case Integer i when i > 0 -> "positive int";
case Integer i -> "non-positive int";
case Long l when l > 0 -> "positive long";
default -> "other number";
};
}
7. Record Patterns in Enhanced for
You can destructure records directly in loops.
record Point(int x, int y) {}
List<Point> points = List.of(new Point(1, 2), new Point(3, 4));
for (Point(int x, int y) : points) {
System.out.println("x=" + x + ", y=" + y);
}
8. Primitive Patterns
Pattern matching extended to primitives. This allows more concise numeric handling.
static String describe(Object obj) {
return switch (obj) {
case int i -> "int: " + i;
case long l -> "long: " + l;
case double d -> "double: " + d;
case null -> "null value";
default -> "unknown";
};
}
This eliminates boxing/unboxing overhead and makes pattern matching truly universal.
Top comments (0)