Tipos Sellados de Java en Profundidad
Modela jerarquías de tipos cerradas en Java con clases e interfaces selladas, especialmente con coincidencia de patrones.
Las clases e interfaces selladas (finalizadas en Java 17) permiten que un tipo declare exactamente qué otros tipos pueden extenderlo o implementarlo. En lugar de una jerarquía abierta que cualquiera puede subclasificar, escribes un conjunto cerrado sobre el que el compilador puede razonar. Esa única garantía — estos y solo estos — es lo que impulsa la coincidencia exhaustiva de patrones y hace que las jerarquías de clases con forma de datos sean seguras de modelar.
Un tipo sellado es el socio natural de los records. Los records te dan los datos; el sellado te da el conjunto cerrado de casos. Juntos llevan los tipos de datos algebraicos (el "tipo suma" que quizás conozcas de Kotlin, Rust o Scala) al Java estándar, y cambian cómo se comporta un switch sobre una jerarquía.
Este capítulo cubre cómo sellar un tipo con permits, la elección obligatoria de final / sealed / non-sealed que cada subtipo debe hacer, por qué una jerarquía cerrada desbloquea un switch exhaustivo sin default, y cómo el sellado se combina con la deconstrucción de records y los patrones con guardas. Se basa en interfaces y herencia.
Sellar un Tipo con permits
Un tipo se vuelve sellado con el modificador sealed y una cláusula permits que lista cada subtipo directo. Ninguna otra clase puede unirse a la jerarquía, incluso en el mismo paquete. Los subtipos permitidos deben ser accesibles para el tipo sellado y, en el módulo sin nombre, vivir en el mismo paquete (o el mismo módulo).
public sealed interface Payment
permits Cash, Card, BankTransfer {}
public record Cash(int amount) implements Payment {}
public record Card(String number, int amount) implements Payment {}
public record BankTransfer(String iban, int amount) implements Payment {}Si un subtipo se encuentra en el mismo archivo fuente, la cláusula permits es opcional — el compilador la infiere del archivo. Solo necesitas escribir permits explícitamente cuando los subtipos viven en archivos separados.
// Same file: permits is inferred, so it can be omitted.
sealed interface Expr {
record Num(int value) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
}La Regla de final, sealed y non-sealed
Cada subtipo permitido debe indicar por sí mismo cómo su parte de la jerarquía está cerrada. El compilador obliga a elegir: cada subtipo directo debe declararse como final, sealed o non-sealed. No hay opción de "no hacer nada" — omitir el modificador es un error de compilación.
| Modificador | Significado para el subtipo |
|---|---|
final | El subtipo no puede extenderse más. Los records son implícitamente final. |
sealed | El subtipo está cerrado y proporciona su propia lista permits. |
non-sealed | El subtipo reabre la jerarquía — cualquiera puede extenderlo de nuevo. |
public sealed class Shape permits Circle, Polygon, Freeform {}
public final class Circle extends Shape {} // closed here
public sealed class Polygon extends Shape // closed, but to a set
permits Triangle, Rectangle {}
public non-sealed class Freeform extends Shape {} // reopened: any subclass allowed
public final class Triangle extends Polygon {}
public final class Rectangle extends Polygon {}non-sealed es la válvula de escape: permite que una rama de una jerarquía de otro modo cerrada permanezca abierta para extensión. Úsala con moderación, porque renuncia a la garantía de exhaustividad para esa rama.
Por Qué el Sellado Habilita un Switch Exhaustivo
La recompensa de cerrar una jerarquía es que el compilador conoce la lista completa de casos. Un switch sobre un tipo sellado que cubre cada subtipo permitido es exhaustivo, por lo que no se escribe una rama default. Mejor aún, si alguien añade más adelante un nuevo subtipo permitido, todos los switch no exhaustivos dejan de compilar — el compilador te señala el código que olvidó el nuevo caso.
sealed interface Payment permits Cash, Card, BankTransfer {}
record Cash(int amount) implements Payment {}
record Card(String number, int amount) implements Payment {}
record BankTransfer(String iban, int amount) implements Payment {}
static String fee(Payment p) {
return switch (p) { // no default needed
case Cash c -> "no fee";
case Card c -> "2% card fee";
case BankTransfer b -> "flat fee";
};
}Elimina el caso BankTransfer y el código no compilará: "the switch expression does not cover all possible input values." Ese empuje en tiempo de compilación es la razón central para sellar una jerarquía.
Records, Deconstrucción y Guardas
Dado que los subtipos permitidos suelen ser records, puedes combinar el sellado con patrones de deconstrucción de records y patrones con guardas (when) — consulta coincidencia de patrones para la funcionalidad completa. La deconstrucción vincula los componentes del record directamente en la etiqueta case; una guarda añade una condición booleana. El orden importa: los casos con guarda más específicos deben ir antes del caso sin guarda del mismo tipo.
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
static String describe(Shape s) {
return switch (s) {
case Circle(double r) when r > 10 -> "big circle";
case Circle(double r) -> "circle r=" + r;
case Rectangle(double w, double h) when w == h -> "square";
case Rectangle(double w, double h) -> "rectangle";
};
}El compilador sigue tratando esto como exhaustivo: cada subtipo permitido es igualado por al menos una etiqueta sin guarda, por lo que el switch completo es total aunque algunas etiquetas tengan guardas.
Un Ejemplo Trabajado
El ejemplo ejecutable a continuación une todo: una interfaz Shape sellada con tres subtipos de record, un switch exhaustivo para el área, un switch de deconstrucción con guarda para la descripción, y un vistazo a los metadatos de sellado a través de la reflexión. Usa solo el JDK, así que se ejecuta tal como está.
Qué destacar de la ejecución:
- El
switchde área no tiene ramadefault— porqueShapeestá sellada, cubrir los tres records ya es exhaustivo. describeimprimebig circle r=12.0solo para el círculo de radio 12, probando que la guardawhen r > 10se evalúa antes de la etiquetaCirclesin guarda.- El rectángulo con lado 5 imprime
square side=5.0, mostrando que la guardaw == hgana sobre el casoRectanglesimple que le sigue. - El área total (525.96) se acumula a lo largo de cada subtipo de record, confirmando que un único bucle polimórfico maneja toda la jerarquía cerrada.
Shape.class.isSealed()devuelvetrueygetPermittedSubclasses()lista Circle, Rectangle y Triangle — el conjuntopermitssobrevive en los metadatos en tiempo de ejecución.