Skip to main content
Java

Java 15: Sealed Classes and Text Blocks Finalization

Ravinder··7 min read
JavaSealed ClassesInheritance ControlJava 15
Share:
Java 15: Sealed Classes and Text Blocks Finalization

Java 15: Sealed Classes and Domain Modeling

Java 15 (September 2020) introduced sealed classes, a powerful feature for precise control over inheritance. This allows developers to explicitly define which classes can extend a class or implement an interface.

1. Sealed Classes and Interfaces

Control inheritance by explicitly allowing only certain subclasses.

Before Sealed Classes:

// Anyone can extend this class
public class Shape {
    abstract double getArea();
}
 
// Multiple implementations - hard to track
public class Circle extends Shape { }
public class Rectangle extends Shape { }
public class Triangle extends Shape { }
// Someone could add: public class InvalidShape extends Shape {}
// No way to prevent it!

After Java 15:

// Only specific classes can extend
public sealed class Shape permits Circle, Rectangle, Triangle {
    public abstract double getArea();
}
 
// Only these can extend Shape
public final class Circle extends Shape {
    private double radius;
    
    public double getArea() {
        return Math.PI * radius * radius;
    }
}
 
public final class Rectangle extends Shape {
    private double width, height;
    
    public double getArea() {
        return width * height;
    }
}
 
public final class Triangle extends Shape {
    private double base, height;
    
    public double getArea() {
        return base * height / 2;
    }
}
 
// ❌ This will NOT compile!
// public class InvalidShape extends Shape {}
// Error: class is not allowed to extend sealed class 'Shape'

Sealed Interfaces:

// Sealed interface
public sealed interface PaymentMethod permits CreditCard, PayPal, ApplePay {
    void pay(double amount);
}
 
public record CreditCard(String cardNumber, String expiryDate) implements PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("Paying $" + amount + " with credit card");
    }
}
 
public record PayPal(String email) implements PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("Paying $" + amount + " with PayPal");
    }
}
 
public record ApplePay(String token) implements PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("Paying $" + amount + " with Apple Pay");
    }
}

2. Sealed Classes with Pattern Matching

Use sealed classes with pattern matching for exhaustive checks.

// Sealed class hierarchy perfect for pattern matching
public sealed interface Vehicle permits Car, Truck, Motorcycle {}
 
public record Car(int seats, double fuelCapacity) implements Vehicle {}
public record Truck(int axles, double maxLoad) implements Vehicle {}
public record Motorcycle(boolean hasSidecar) implements Vehicle {}
 
// Pattern matching on sealed class - compiler ensures all cases covered
public double getInsuranceRate(Vehicle vehicle) {
    return switch (vehicle) {
        case Car car -> 800.0 * car.seats();
        case Truck truck -> 1500.0 * truck.axles();
        case Motorcycle moto -> 400.0 * (moto.hasSidecar() ? 1.2 : 1.0);
        // Compiler error if any case is missing!
    };
}

3. Text Blocks are Finalized

Text blocks are now a standard feature (no preview needed).

// Standard feature - not preview
String queryEngine = """
    SELECT u.user_id,
           u.username,
           COUNT(o.order_id) as total_orders,
           SUM(o.amount) as total_spent
    FROM users u
    LEFT JOIN orders o ON u.user_id = o.user_id
    WHERE u.created_at >= DATE_SUB(NOW(), INTERVAL 1 MONTH)
    GROUP BY u.user_id
    HAVING total_spent > ?
    ORDER BY total_spent DESC
    LIMIT 100;
    """;
 
String templateEmail = """
    Dear ${customer.name},
    
    Thank you for your order #${order.id}.
    
    Order Details:
    - Total Amount: $${order.total}
    - Estimated Delivery: ${order.deliveryDate}
    
    Best regards,
    The Customer Service Team
    """;
 
// Practical: Configuration files as strings
String appConfig = """
    app:
      name: "MyApplication"
      version: "1.0.0"
      settings:
        debug: true
        maxConnections: 100
        timeout: 30000
    """;

4. Records Enhancement

Records got improvements for better usability.

// Records can be generic
public record Container<T>(T value, String description) {}
 
// Using generic records
Container<String> stringContainer = new Container<>("Hello", "Greeting");
Container<Integer> intContainer = new Container<>(42, "The Answer");
 
// Records can have instance methods
public record Point(double x, double y) {
    public double distanceFrom(Point other) {
        double dx = this.x - other.x;
        double dy = this.y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }
    
    public String toWKT() {
        return String.format("POINT (%f %f)", x, y);
    }
}
 
// Usage
Point p1 = new Point(0, 0);
Point p2 = new Point(3, 4);
System.out.println(p1.distanceFrom(p2)); // 5.0
System.out.println(p2.toWKT()); // POINT (3.000000 4.000000)

5. Pattern Matching for switch (Preview)

Initial support for pattern matching in switch statements.

// Pattern matching in switch (Preview - requires --enable-preview)
public String describeValue(Object obj) {
    return switch (obj) {
        case Integer i -> String.format("Integer: %d", i);
        case Long l -> String.format("Long: %d", l);
        case Double d -> String.format("Double: %.2f", d);
        case String s -> String.format("String: %s", s);
        case null -> "null value";
        default -> "Unknown type";
    };
}
 
// With guards
public String categorizeNumber(Integer number) {
    return switch (number) {
        case Integer i when i < 0 -> "Negative";
        case Integer i when i == 0 -> "Zero";
        case Integer i when i < 10 -> "Single digit";
        case Integer i when i < 100 -> "Double digit";
        default -> "Large number";
    };
}

6. Hidden Classes

Create hidden classes that cannot be discovered via reflection.

// Advanced: Used by frameworks for dynamic proxies
import java.lang.invoke.MethodHandles;
 
public class HiddenClassExample {
    public static Class<?> createHiddenClass(Class<?> baseClass) throws Throwable {
        // Complex bytecode manipulation
        byte[] bytecode = generateBytecode(baseClass);
        
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        return lookup.defineHiddenClass(
            bytecode,
            true  // Make searchable in dumps
        ).lookupClass();
    }
    
    private static byte[] generateBytecode(Class<?> baseClass) {
        // Bytecode generation logic here
        return new byte[]{};
    }
}

Developer Impact

Positive:

  • Type Safety: Sealed classes enforce domain constraints
  • Exhaustive Checking: Compiler ensures all cases handled
  • Better Design: Forces explicit thinking about inheritance
  • Pattern Matching: More ergonomic control flow

Learning Curve:

  • Sealed classes require design thinking
  • Pattern matching is still evolving
  • Some may find it overly restrictive

Pros and Cons

Pros ✅

  • Sealed Classes: Explicit control over inheritance hierarchy
  • Domain Modeling: Perfect for closed hierarchies (Shape, Vehicle)
  • Maintainability: Prevents unwanted subclasses
  • Pattern Matching: Works perfectly with sealed types
  • Compiler Safety: Warns if pattern matching not exhaustive
  • Records Generics: Better support for generic data classes
  • Text Blocks Finalized: Production-ready multi-line strings

Cons ❌

  • Learning Curve: Sealed classes require architectural thinking
  • Restrictive: Cannot extend sealed class unless explicitly permitted
  • Not for Open APIs: Sealed classes don't work for frameworks
  • Pattern Matching Preview: Still evolving, syntax may change
  • No LTS: Short support window for non-LTS release

Architectural Example

// Great for domain-driven design
public sealed interface OrderStatus permits 
    PendingOrder, ConfirmedOrder, ShippedOrder, DeliveredOrder, CancelledOrder {}
 
public record PendingOrder(LocalDateTime createdAt) implements OrderStatus {}
public record ConfirmedOrder(LocalDateTime confirmedAt) implements OrderStatus {}
public record ShippedOrder(LocalDateTime shippedAt, String trackingNumber) implements OrderStatus {}
public record DeliveredOrder(LocalDateTime deliveredAt) implements OrderStatus {}
public record CancelledOrder(LocalDateTime cancelledAt, String reason) implements OrderStatus {}
 
// State machines become type-safe
public class OrderProcessor {
    public String getStatusMessage(Order order) {
        return switch (order.status()) {
            case PendingOrder p -> "Your order is pending confirmation";
            case ConfirmedOrder c -> "Your order has been confirmed";
            case ShippedOrder s -> "Shipped! Track: " + s.trackingNumber();
            case DeliveredOrder d -> "Delivered on " + d.deliveredAt();
            case CancelledOrder c -> "Cancelled: " + c.reason();
        };
    }
}

Conclusion

Java 15 introduces sealed classes, a sophisticated feature for building robust, maintainable domain models. Combined with records and pattern matching, Java becomes a powerful language for precise type modeling. Sealed classes are particularly useful for building correct enum-like types that go beyond what plain enums can express.

Key Takeaways:

  • Use sealed classes for controlled inheritance hierarchies
  • Perfect for domain-driven design and state machines
  • Combine with pattern matching for exhaustive checking
  • Text blocks are now standard for all multi-line strings
  • Consider Java 15 as a stepping stone toward Java 17 LTS