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