Java Records, Sealed Classes, and Pattern Matching, when combined, offer a powerful, declarative, and type-safe way to model and process data.

Let’s see this in action with a simple Shape hierarchy. Imagine we want to represent geometric shapes and calculate their area.

// Define a sealed abstract class
sealed abstract class Shape permits Circle, Rectangle {
    abstract double area();
}

// Define a record for Circle
record Circle(double radius) extends Shape {
    @Override
    double area() {
        return Math.PI * radius * radius;
    }
}

// Define a record for Rectangle
record Rectangle(double width, double height) extends Shape {
    @Override
    double area() {
        return width * height;
    }
}

public class ShapeProcessor {
    public static void main(String[] args) {
        Shape circle = new Circle(5.0);
        Shape rectangle = new Rectangle(4.0, 6.0);

        processShape(circle);
        processShape(rectangle);
    }

    public static void processShape(Shape shape) {
        // Using pattern matching with sealed classes and records
        switch (shape) {
            case Circle c -> System.out.println("Processing a Circle with radius " + c.radius() + " and area " + c.area());
            case Rectangle r -> System.out.println("Processing a Rectangle with width " + r.width() + ", height " + r.height() + " and area " + r.area());
            // No default case needed because Shape is sealed, and all permitted types are covered.
        }
    }
}

When ShapeProcessor.main runs, you’ll see output like this:

Processing a Circle with radius 5.0 and area 78.53981633974483
Processing a Rectangle with width 4.0, height 6.0 and area 24.0

This isn’t just about cleaner syntax; it fundamentally changes how you think about data modeling and handling variations. The Shape abstract class is sealed, meaning only Circle and Rectangle (declared in the permits clause) can extend it. This is crucial for exhaustive pattern matching. If you try to add a Triangle class that extends Shape without updating the permits clause, the compiler will flag an error.

Records (Circle, Rectangle) provide a concise way to create immutable data carriers. They automatically generate constructors, equals(), hashCode(), and toString() methods, and crucially for pattern matching, accessor methods like radius(), width(), and height().

The processShape method demonstrates the magic. The switch expression (or statement) uses pattern matching. When shape is a Circle, it’s automatically deconstructed into a Circle variable c, giving you direct access to its radius() and area(). The same happens for Rectangle with variable r. Because Shape is sealed and all permitted subtypes are explicitly handled in the switch, the compiler guarantees that all possible Shape instances will be covered. You don’t need a default case, and the compiler will warn you if you miss a subtype. This eliminates a whole class of runtime NullPointerExceptions or ClassCastExceptions that would have plagued older Java versions.

The core problem this combination solves is managing variations in data while ensuring type safety and code clarity. Before these features, you’d likely use a class hierarchy with inheritance, but then you’d need instanceof checks and casts to differentiate between subtypes, which is error-prone. Or you might use discriminated unions (common in functional languages) but Java didn’t have a direct, idiomatic way to do that. Sealed classes provide the restricted inheritance, records provide the data structure, and pattern matching provides the safe, declarative way to deconstruct and process that data based on its specific type.

The most surprising thing about this approach is how it merges object-oriented and functional programming paradigms. You get the benefits of immutable data structures (records) and restricted hierarchies (sealed classes) that are common in functional programming, but within the familiar Java syntax and object-oriented model. The compiler’s ability to enforce exhaustiveness in pattern matching on sealed types is a massive win for robustness.

The next hurdle you’ll likely encounter is handling more complex data structures, perhaps nested records or collections of these sealed types.

Want structured learning?

Take the full Java course →