Skip to content

Java Records, Pattern Matching, and Sealed Classes: Modern Java Features

Published on Jul 30, 2025 | approx. 4 min read |

Java has evolved significantly in recent years. Since Java 16 (Records), Java 17 (Sealed Classes), and Java 21 (Pattern Matching for switch as a standard feature), modern Java has become noticeably more concise and expressive. This article demonstrates the practical use of these features — with comparisons to PHP 8.x for PHP developers.

Java Records (Stable Since Java 16)

Before Records, writing a simple data class required a lot of boilerplate:

// Old: Boilerplate hell
public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int x() { return x; }
    public int y() { return y; }

    @Override
    public boolean equals(Object obj) { /* ... */ }

    @Override
    public int hashCode() { /* ... */ }

    @Override
    public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}

With Records:

// New: everything in a single line
public record Point(int x, int y) {}

The compiler automatically generates: constructor, getters (without the get prefix), equals(), hashCode(), and toString().

Records in Spring Boot

// DTO as a Record — ideal for API requests/responses
public record ProductDto(
    String name,
    BigDecimal price,
    String category
) {}

// Controller
@PostMapping("/products")
public ResponseEntity<Product> create(@RequestBody @Valid ProductDto dto) {
    Product product = productService.create(dto.name(), dto.price(), dto.category());
    return ResponseEntity.status(201).body(product);
}

Compact Constructors for Validation

public record Email(String value) {
    // Compact Constructor — code runs before field assignment
    public Email {
        if (value == null || !value.contains("@")) {
            throw new IllegalArgumentException("Invalid email address: " + value);
        }
        value = value.toLowerCase().strip(); // Normalization
    }
}

// Comparison: PHP 8.x Readonly Properties
// class Email {
//     public function __construct(
//         public readonly string $value
//     ) {
//         if (!str_contains($value, '@')) {
//             throw new \InvalidArgumentException('Invalid email address');
//         }
//     }
// }

Sealed Classes and Interfaces (Since Java 17)

Sealed Classes allow you to control a class hierarchy — only explicitly permitted subclasses are allowed.

// Sealed Interface — only these three implementations are permitted
public sealed interface Shape
    permits Circle, Rectangle, Triangle {}

public record Circle(double radius) implements Shape {}
public record Rectangle(double width, double height) implements Shape {}
public record Triangle(double base, double height) implements Shape {}

This is similar to PHP 8.1 Enums, but more flexible since each implementation can have its own fields.

Comparison with PHP Enum

// PHP 8.1 Enum
enum Shape {
    case Circle;
    case Rectangle;
    case Triangle;
}
// Java Sealed Interface — more flexibility
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}

Pattern Matching for switch (Java 21, Standard Feature)

Pattern Matching for switch is the feature that makes Sealed Classes truly useful. Instead of long instanceof cascades:

// Old: instanceof cascade
double area(Shape shape) {
    if (shape instanceof Circle c) {
        return Math.PI * c.radius() * c.radius();
    } else if (shape instanceof Rectangle r) {
        return r.width() * r.height();
    } else if (shape instanceof Triangle t) {
        return 0.5 * t.base() * t.height();
    }
    throw new IllegalStateException("Unknown Shape: " + shape);
}

// New: Pattern Matching switch (Java 21)
double area(Shape shape) {
    return switch (shape) {
        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();
        // No default needed — the compiler knows all cases are covered!
    };
}

The compiler checks exhaustiveness: since Shape is sealed and all subclasses are covered, no default case needs to be specified. When a new implementation of Shape is added, a compile error occurs — just like PHP Enums in match expressions.

Guards in Pattern Matching

String classify(Object obj) {
    return switch (obj) {
        case Integer i when i < 0  -> "Negative number";
        case Integer i when i == 0 -> "Zero";
        case Integer i             -> "Positive number: " + i;
        case String s when s.isEmpty() -> "Empty string";
        case String s              -> "String: " + s;
        case null                  -> "null";
        default                    -> "Other: " + obj.getClass().getSimpleName();
    };
}

The when guard is equivalent to PHP's match with true conditions.

Text Blocks (Java 15) — for Multi-Line Strings

// Old
String json = "{\n" +
    "  \"name\": \"Produkt\",\n" +
    "  \"price\": 19.99\n" +
    "}";

// New: Text Block
String json = """
    {
      "name": "Produkt",
      "price": 19.99
    }
    """;

Analogous to PHP Heredoc:

$json = <<<JSON
{
  "name": "Produkt",
  "price": 19.99
}
JSON;

Practical Example: Event System with Sealed Interfaces

// Events as Sealed Interface + Records
public sealed interface OrderEvent
    permits OrderPlaced, OrderShipped, OrderCancelled {}

public record OrderPlaced(String orderId, BigDecimal total) implements OrderEvent {}
public record OrderShipped(String orderId, String trackingNumber) implements OrderEvent {}
public record OrderCancelled(String orderId, String reason) implements OrderEvent {}

// Handler
public class OrderEventHandler {
    public void handle(OrderEvent event) {
        switch (event) {
            case OrderPlaced placed ->
                sendConfirmationEmail(placed.orderId(), placed.total());
            case OrderShipped shipped ->
                sendTrackingEmail(shipped.orderId(), shipped.trackingNumber());
            case OrderCancelled cancelled ->
                processRefund(cancelled.orderId(), cancelled.reason());
        }
        // No default — the compiler ensures all events are handled
    }
}

Conclusion

Records, Sealed Classes, and Pattern Matching make Java more expressive and safer. Records eliminate boilerplate for data classes. Sealed Classes make type hierarchies controllable. Pattern Matching for switch enables exhaustive, type-safe case differentiation without runtime errors.

For PHP developers, these features are not unfamiliar: Records resemble PHP readonly classes, Sealed Classes overlap with PHP Enums, and switch pattern matching is similar to PHP's match expression. The key difference: Java checks exhaustiveness at compile time, which completely eliminates an entire class of errors.

Thomas Wunner

Thomas Wunner

Certified IT specialist for application development with an instructor qualification and over 14 years of experience building scalable web applications with Symfony and Shopware. When not coding, Thomas volunteers as a lifeguard with the Wasserwacht, performs as a DJ, and explores the countryside on his motorbike.

Comments

Comments are provided by Remark42. By loading comments, data is transmitted to our comment server.