DEV Community

Sadiul Hakim
Sadiul Hakim

Posted on

Modern Java Pattern Matching in one place

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());
}
Enter fullscreen mode Exit fullscreen mode

With Pattern Matching

if (obj instanceof String s) {
    System.out.println(s.toUpperCase());
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Eliminates boilerplate casting.
  • The variable s is only available inside the if block.
  • Works with flow scoping: if the compiler knows obj is a String, 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();
    };
}
Enter fullscreen mode Exit fullscreen mode

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";
    };
}
Enter fullscreen mode Exit fullscreen mode

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 + ")");
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    };
}
Enter fullscreen mode Exit fullscreen mode

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";
    };
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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";
    };
}
Enter fullscreen mode Exit fullscreen mode

This eliminates boxing/unboxing overhead and makes pattern matching truly universal.

Top comments (0)