Zum Inhalt springen

Java Records, Pattern Matching und Sealed Classes: Moderne Java-Features

Veröffentlicht am 30. Juli 2025 | ca. 4 Min. Lesezeit |

Java hat sich in den letzten Jahren erheblich weiterentwickelt. Seit Java 16 (Records), Java 17 (Sealed Classes) und Java 21 (Pattern Matching für switch als Standard-Feature) ist modernes Java deutlich kompakter und ausdrucksstärker. Dieser Artikel zeigt die praktische Anwendung dieser Features — mit Vergleichen zu PHP 8.x für PHP-Entwickler.

Java Records (ab Java 16 stabil)

Vor Records musste man für eine einfache Datenklasse viel Boilerplate schreiben:

// Alt: Boilerplate-Hölle
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 + "]"; }
}

Mit Records:

// Neu: alles in einer Zeile
public record Point(int x, int y) {}

Der Compiler generiert automatisch: Konstruktor, Getter (ohne get-Präfix), equals(), hashCode() und toString().

Records in Spring Boot

// DTO als Record — ideal für 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 für Validierung

public record Email(String value) {
    // Compact Constructor — Code wird vor der Feldzuweisung ausgeführt
    public Email {
        if (value == null || !value.contains("@")) {
            throw new IllegalArgumentException("Ungültige E-Mail-Adresse: " + value);
        }
        value = value.toLowerCase().strip(); // Normalisierung
    }
}

// Vergleich: PHP 8.x Readonly Properties
// class Email {
//     public function __construct(
//         public readonly string $value
//     ) {
//         if (!str_contains($value, '@')) {
//             throw new \InvalidArgumentException('Ungültige E-Mail-Adresse');
//         }
//     }
// }

Sealed Classes und Interfaces (ab Java 17)

Sealed Classes erlauben es, die Hierarchie einer Klasse zu kontrollieren — nur explizit erlaubte Subklassen sind möglich.

// Sealed Interface — nur diese drei Implementierungen sind erlaubt
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 {}

Das ist ähnlich zu PHP 8.1 Enums, aber flexibler, da jede Implementierung eigene Felder haben kann.

Vergleich mit PHP Enum

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

Pattern Matching für switch (Java 21, Standard-Feature)

Pattern Matching für switch ist das Feature, das Sealed Classes erst wirklich nützlich macht. Statt langer instanceof-Kaskaden:

// Alt: instanceof-Kaskade
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("Unbekannte Shape: " + shape);
}

// Neu: 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();
        // Kein default nötig — Compiler weiß, dass alle Fälle abgedeckt sind!
    };
}

Der Compiler prüft Exhaustiveness: Da Shape sealed ist und alle Subklassen abgedeckt sind, muss kein default-Fall angegeben werden. Wenn eine neue Implementierung von Shape hinzukommt, gibt es einen Compile-Fehler — genau wie PHP Enums in match-Ausdrücken.

Guards im Pattern Matching

String classify(Object obj) {
    return switch (obj) {
        case Integer i when i < 0  -> "Negative Zahl";
        case Integer i when i == 0 -> "Null";
        case Integer i             -> "Positive Zahl: " + i;
        case String s when s.isEmpty() -> "Leerer String";
        case String s              -> "String: " + s;
        case null                  -> "null";
        default                    -> "Sonstiges: " + obj.getClass().getSimpleName();
    };
}

Das when-Guard entspricht PHP's match mit true-Bedingungen.

Text Blocks (Java 15) — für mehrzeilige Strings

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

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

Analog zu PHP Heredoc:

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

Praktisches Beispiel: Event-System mit Sealed Interfaces

// Events als 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());
        }
        // Kein default — der Compiler stellt sicher, dass alle Events behandelt werden
    }
}

Fazit

Records, Sealed Classes und Pattern Matching machen Java ausdrucksstärker und sicherer. Records eliminieren Boilerplate für Datenklassen. Sealed Classes machen Typen-Hierarchien kontrollierbar. Pattern Matching für switch ermöglicht erschöpfende, typsichere Fallunterscheidung ohne Laufzeit-Fehler.

Für PHP-Entwickler sind diese Features nicht fremd: Records ähneln PHP Readonly-Klassen, Sealed Classes haben Überschneidungen mit PHP Enums, und switch-Pattern-Matching ähnelt PHP's match-Ausdruck. Der Hauptunterschied: Java prüft Exhaustiveness zur Compile-Zeit, was eine Fehlerklasse komplett eliminiert.

Thomas Wunner

Thomas Wunner

Fachinformatiker für Anwendungsentwicklung mit Ausbildereignungsprüfung und über 14 Jahre Erfahrung im Aufbau skalierbarer Webanwendungen mit Symfony und Shopware. Abseits der Tastatur ist Thomas als Rettungsschwimmer in der Wasserwacht aktiv, legt als DJ auf und erkundet die Umgebung auf dem Motorrad.

Kommentare

Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.