Clases selladas en Java
Restringe qué clases pueden extender o implementar un tipo en Java usando clases selladas y la cláusula permits.
Una clase o interfaz sellada restringe quién puede extenderla o implementarla a una lista fija y nombrada de subtipos. final dice "nadie puede extenderme." sealed dice "solo estas clases específicas pueden." Otorga una jerarquía cerrada — el compilador conoce toda la familia de antemano, lo que habilita switch exhaustivos y el modelado disciplinado de formas "uno de N".
Sin sellado, una abstract class Shape está abierta al mundo: cualquiera con acceso al tipo puede escribir class Banana extends Shape. Con sealed, el autor de Shape declara exactamente qué subtipos existen, y añadir uno requiere editar el padre.
La sintaxis básica
Una clase sellada lista sus subtipos permitidos con permits:
public sealed class Shape
permits Circle, Square, Triangle {
// common state and behavior
}Cada subtipo permitido debe declarar a su vez qué hace con el sellado — uno de final, sealed (con su propia lista de permits), o non-sealed:
public final class Circle extends Shape { /* leaf */ }
public final class Square extends Shape { /* leaf */ }
public non-sealed class Triangle extends Shape { /* re-opens the door */ }final— sin más subclases; esta es una hoja en la jerarquía.sealed— extiende el mismo modelo; tiene su propia listapermits.non-sealed— reabre la jerarquía; cualquiera puede extenderTriangle. Útil cuando se quiere una familia de nivel superior cerrada con una rama abierta.
Un tipo sellado sin modificador en un subtipo es un error de compilación — el compilador te obliga a elegir.
Interfaces selladas
Las interfaces siguen las mismas reglas y suelen ser la opción más natural para modelar familias de casos:
public sealed interface Result<T>
permits Success, Failure {}
public record Success<T>(T value) implements Result<T> {}
public record Failure<T>(String message) implements Result<T> {}Combinado con records, se obtiene algo cercano al "tipo suma" o "unión etiquetada" de los lenguajes funcionales — una lista cerrada de alternativas con nombre, cada una con sus propios datos.
Mismo módulo, mismo paquete (o permits explícito)
Los subtipos permitidos deben ser accesibles para la declaración sellada en tiempo de compilación. La configuración más sencilla es poner la clase sellada y sus subtipos permitidos en el mismo archivo fuente — entonces incluso se puede omitir permits, porque el compilador lo infiere:
public sealed interface Tree {
record Leaf(int value) implements Tree {}
record Node(Tree left, Tree right) implements Tree {}
}Si están en archivos separados, deben estar en el mismo paquete (o, en un proyecto modular, en el mismo módulo), y la cláusula permits es obligatoria.
El beneficio: switch exhaustivo
El compilador conoce todos los posibles subtipos de un tipo sellado. Eso permite que switch garantice exhaustividad sin default:
double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square q -> q.side() * q.side();
case Triangle t -> 0.5 * t.base() * t.height();
};
}Si luego se añade un Hexagon permitido, este switch deja de compilar en todos los lugares donde aparece hasta que se gestione el nuevo caso. Esa es exactamente la red de seguridad que default destruiría silenciosamente.
Reglas que el compilador impone
Algunas restricciones son fáciles de ignorar por error:
- Cada subtipo permitido debe extender o implementar directamente el tipo sellado. No se puede listar un nieto en
permits— solo subtipos inmediatos. - Cada subtipo permitido debe elegir un modificador:
final,sealed, onon-sealed. Olvidarlo es un error de compilación. - Los tipos permitidos deben ser localizables en tiempo de compilación — mismo archivo, mismo paquete, o el mismo módulo con nombre. Un tipo sellado no puede permitir una clase en un módulo no relacionado.
- Los records son implícitamente
final, por lo que un record puede ser un subtipo permitido sin escribirfinalexplícitamente. Por eso la combinación de interfaz sellada con records es tan limpia.
El modificador non-sealed es la vía de escape deliberada. Úsalo cuando la mayor parte de una jerarquía deba permanecer cerrada pero una rama sea un punto de extensión previsto:
public sealed interface Vehicle permits Car, Truck, CustomBuild {}
public record Car(int doors) implements Vehicle {}
public record Truck(double tons) implements Vehicle {}
// Re-opened: third parties may extend this branch.
public non-sealed interface CustomBuild extends Vehicle {}Como CustomBuild es non-sealed, un switch sobre Vehicle todavía necesita un caso de reserva para él — el compilador ya no puede probar que esa rama es exhaustiva.
Cuándo usar sellado
Recurre al sellado cuando la abstracción es genuinamente un conjunto cerrado de casos:
- Nodos de AST o expresiones (
Literal,Add,Multiply...). - Resultados de dominio que son "éxito o uno de estos fallos."
- Jerarquías de comandos/eventos donde cada consumidor necesita gestionar cada caso.
No selles tipos que son puntos de extensión — interfaces de plugins, ganchos de frameworks, cualquier cosa que los llamantes deban poder extender. Sellarlos anula su propósito.
Un ejemplo práctico
Qué sigue
El sellado restringe la lista de subtipos. El siguiente capítulo trata sobre preguntar, en tiempo de ejecución, cuál subtipo tienes realmente — el operador instanceof y su forma moderna de coincidencia de patrones, que es lo que hace que el switch anterior sea tan conciso. Continúa en Operador instanceof de Java.