The combination of Records (Product Types) and Sealed Classes (Sum Types) allows Java to support formal Algebraic Data Types (ADTs). This shift enables developers to move domain invariants into the type system, replacing runtime checks with compile-time safety.
A Record is a Product Type because its state space is the product of its components.
final. Accessors are provided.// Product Type: State space = int * String
public record User(int id, String name) {}
A Sealed class/interface is a Sum Type because its state space is the sum of its permitted subtypes.
permits clause closes the hierarchy.switch expression.Encoding an order lifecycle as an ADT prevents invalid states (e.g., a "Cancelled" order having a "TrackingNumber").
public sealed interface OrderState
permits Created, Shipped, Delivered, Cancelled {}
public record Created(Instant timestamp) implements OrderState {}
public record Shipped(Instant timestamp, String trackingId) implements OrderState {}
public record Delivered(Instant timestamp, String signedBy) implements OrderState {}
public record Cancelled(Instant timestamp, String reason) implements OrderState {}
public String getDisplayStatus(OrderState state) {
return switch (state) {
case Created c -> "Order placed at " + c.timestamp();
case Shipped s -> "In transit. Tracking: " + s.trackingId();
case Delivered d -> "Delivered to " + d.signedBy();
case Cancelled c -> "Cancelled: " + c.reason();
// No 'default' needed! Adding a new state will break compilation here.
};
}
Java 21 allows deconstructing records directly in the case label, including When Guards.
public void process(OrderState state) {
switch (state) {
case Shipped(var ts, var id) when id.startsWith("FEDEX") ->
trackViaFedex(id);
case Shipped(var ts, var id) ->
trackGeneric(id);
default -> {}
}
}
public record PositiveAmount(double value) {
public PositiveAmount {
if (value <= 0) throw new IllegalArgumentException();
}
}
See Also: