Clases sealed
Las clases sealed (o clases selladas) sirven para restringir qué clases pueden heredar de una clase o implementar una interfaz específica. Esta funcionalidad proporciona control sobre la jerarquía de herencia.
Antes de la introducción de esta funcionalidad, para controlar la jerarquía de clases se podía:
- Declarar la clase como
final
para evitar completamente la herencia - Utilizar modificadores de acceso restrictivos para limitar quién podía extender la clase
- Crear clases internas para mantener la jerarquía dentro de un ámbito controlado
Sin embargo, estas aproximaciones no permitían control sobre exactamente qué clases podían extender una clase base. Las clases sealed resuelven este problema.
La sintaxis básica para declarar una clase sealed es la siguiente:
public sealed class Forma permits Circulo, Rectangulo, Triangulo {
// Implementación de la clase
}
Este código establece que Forma
es una clase sellada que solo puede ser extendida por las clases específicamente mencionadas en la cláusula permits
: Circulo
, Rectangulo
y Triangulo
. Cualquier intento de crear otra clase que extienda Forma
resultará en un error de compilación.
Las interfaces también pueden ser selladas, siguiendo la misma sintaxis:
public sealed interface Operacion permits Suma, Resta, Multiplicacion, Division {
// Definición de la interfaz
}
Las clases sealed se usan mucho cuando se necesita modelar dominios con un conjunto fijo y conocido de variantes, como los tipos de operaciones en una calculadora, las figuras en un sistema de dibujo o los mensajes en un protocolo de comunicación.
La principal ventaja de las clases sealed es que permiten al compilador conocer exhaustivamente todos los posibles subtipos de una clase.
Cláusulas permits y extensiones permitidas
La cláusula permits
especifica explícitamente cuáles son las únicas clases que pueden extender o implementar la clase o interfaz sellada. Esta restricción se aplica en tiempo de compilación, garantizando que la jerarquía de tipos permanezca bajo control.
Cuando se define una clase sealed, existen dos formas de especificar las subclases permitidas:
- Explícitamente mediante la cláusula permits:
public sealed class Vehiculo permits Coche, Motocicleta, Camion {
// Implementación
}
- Implícitamente cuando las subclases están en el mismo archivo:
// Archivo Vehiculo.java
public sealed class Vehiculo {
// Implementación
}
final class Coche extends Vehiculo {
// Implementación
}
final class Motocicleta extends Vehiculo {
// Implementación
}
En el segundo caso, la cláusula permits
es opcional ya que el compilador puede determinar las subclases permitidas examinando las declaraciones dentro del mismo archivo.
Todas las clases mencionadas en la cláusula permits
deben existir y deben estar accesibles desde la clase sealed. Además, si las subclases permitidas están en un paquete diferente al de la clase sealed, se debe utilizar obligatoriamente la cláusula permits
.
Las subclases de una clase sealed deben declarar explícitamente su política de herencia usando uno de estos tres modificadores:
final
: La clase no puede ser extendida más. Esta es la opción más restrictiva y común.
public final class Coche extends Vehiculo {
// No se permite ninguna subclase adicional
}
sealed
: La clase también es sealed y debe tener su propia cláusulapermits
. Esto crea una jerarquía controlada en múltiples niveles.
public sealed class Coche extends Vehiculo permits CocheDeportivo, CocheFamiliar {
// Solo permite determinadas subclases
}
non-sealed
: La clase renuncia a las restricciones y permite cualquier subclase. Esto abre una rama específica de la jerarquía.
public non-sealed class Camion extends Vehiculo {
// Cualquier clase puede extender Camion
}
Si se omite alguno de estos modificadores en una subclase de una clase sealed, se producirá un error de compilación.
Un ejemplo completo de una jerarquía de clases sealed podría ser:
public sealed class Forma permits Circulo, Rectangulo, Poligono {
// Código común a todas las formas
}
public final class Circulo extends Forma {
private final double radio;
// Constructor y métodos
}
public final class Rectangulo extends Forma {
private final double ancho;
private final double alto;
// Constructor y métodos
}
public sealed class Poligono extends Forma permits Triangulo, Cuadrado {
// Código común a polígonos
}
public final class Triangulo extends Poligono {
// Implementación específica
}
public final class Cuadrado extends Poligono {
// Implementación específica
}
En este ejemplo, se establece una jerarquía donde Forma
solo puede ser extendida por tres clases específicas, y una de ellas (Poligono
) establece sus propias restricciones de herencia.
Integración con pattern matching
Guarda tu progreso
Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.
Más de 25.000 desarrolladores ya confían en CertiDevs
Como el compilador conoce exhaustivamente todas las posibles subclases de una clase sealed, puede realizar verificaciones más completas en tiempo de compilación, especialmente en expresiones condicionales.
El pattern matching con instanceof
permite comprobar el tipo de un objeto y realizar una conversión de tipo en una sola operación. Cuando se combina con clases sealed, el compilador puede verificar si se han cubierto todos los casos posibles:
public double calcularArea(Forma forma) {
if (forma instanceof Circulo c) {
return Math.PI * c.getRadio() * c.getRadio();
} else if (forma instanceof Rectangulo r) {
return r.getAncho() * r.getAlto();
} else if (forma instanceof Triangulo t) {
return t.getBase() * t.getAltura() / 2;
} else if (forma instanceof Cuadrado c) {
return c.getLado() * c.getLado();
}
// El compilador puede advertir si falta algún caso
throw new IllegalArgumentException("Forma no reconocida");
}
Se usa mucho con las expresiones switch mejoradas. El compilador puede verificar la exhaustividad de los casos, eliminando la necesidad de un caso default
cuando todos los posibles subtipos han sido cubiertos:
public double calcularArea(Forma forma) {
return switch (forma) {
case Circulo c -> Math.PI * c.getRadio() * c.getRadio();
case Rectangulo r -> r.getAncho() * r.getAlto();
case Triangulo t -> t.getBase() * t.getAltura() / 2;
case Cuadrado c -> c.getLado() * c.getLado();
// No es necesario un caso default
};
}
Si se añade una nueva subclase a la jerarquía sealed, el compilador marcará todos los switches existentes como incompletos, forzando a los desarrolladores a actualizar el código para manejar el nuevo caso.
Para las clases sealed con muchas variantes, los pattern guards (guardas de patrón) pueden ser útiles para refinar aún más las condiciones:
return switch (forma) {
case Circulo c -> Math.PI * c.getRadio() * c.getRadio();
case Rectangulo r when r.getAncho() == r.getAlto() ->
r.getAncho() * r.getAncho(); // Es un cuadrado
case Rectangulo r -> r.getAncho() * r.getAlto();
case Triangulo t when t.esEquilatero() ->
Math.sqrt(3) / 4 * t.getLado() * t.getLado();
case Triangulo t -> t.getBase() * t.getAltura() / 2;
};
Las clases sealed también se integran perfectamente con los record patterns, permitiendo la deconstrucción directa de los datos en la expresión switch
:
sealed interface Mensaje permits TextoMensaje, ImagenMensaje, ArchivoMensaje {}
record TextoMensaje(String contenido, String remitente) implements Mensaje {}
record ImagenMensaje(byte[] datos, String formato, String remitente) implements Mensaje {}
record ArchivoMensaje(String nombre, byte[] contenido, long tamaño) implements Mensaje {}
public void procesarMensaje(Mensaje mensaje) {
switch (mensaje) {
case TextoMensaje(String contenido, String remitente) ->
System.out.println("Texto de " + remitente + ": " + contenido);
case ImagenMensaje(var datos, String formato, var remitente) ->
System.out.println("Imagen " + formato + " de " + remitente);
case ArchivoMensaje(String nombre, var _, long tamaño) ->
System.out.println("Archivo: " + nombre + " (" + tamaño + " bytes)");
}
}
Casos de uso y buenas prácticas
Casos de uso principales
- Modelado de dominios cerrados: Cuando se necesita representar un conjunto fijo y conocido de variantes.
sealed interface EstadoPedido permits Creado, Pagado, EnProceso, Enviado, Entregado, Cancelado {
// Métodos comunes
}
- Implementación de tipos algebraicos de datos (ADTs): Comunes en la programación funcional, permiten modelar datos con variantes bien definidas.
sealed interface Expresion permits Literal, Suma, Resta, Multiplicacion, Division {
double evaluar();
}
record Literal(double valor) implements Expresion {
public double evaluar() { return valor; }
}
record Suma(Expresion izquierda, Expresion derecha) implements Expresion {
public double evaluar() { return izquierda.evaluar() + derecha.evaluar(); }
}
- APIs públicas: Al diseñar bibliotecas, las clases sealed permiten controlar exactamente cómo pueden extenderse las clases expuestas.
- Implementación de patrones de diseño: Ciertos patrones como Visitor o Command se benefician de jerarquías cerradas.
sealed interface Comando permits ComandoGuardar, ComandoAbrir, ComandoExportar {
void ejecutar();
}
- Representación de protocolos de comunicación: Donde los tipos de mensajes están bien definidos y no deberían extenderse arbitrariamente.
Buenas prácticas
- Combinar con records para mayor inmutabilidad: Los records y las clases sealed funcionan excepcionalmente bien juntos, creando definiciones concisas de tipos de datos algebraicos.
sealed interface Resultado<T> permits Exito, Error {
// Métodos comunes
}
record Exito<T>(T valor) implements Resultado<T> {}
record Error(String mensaje, Exception excepcion) implements Resultado<?> {}
- Mantener jerarquías poco profundas: Las jerarquías de clases sealed son más manejables cuando no son demasiado profundas.
- Favorecer subclases finales: A menos que exista una razón específica para permitir más extensibilidad, es preferible declarar las subclases como
final
. - Considerar el impacto en la evolución del código: Si es probable que se añadan nuevas variantes con frecuencia, evaluar si una clase sealed es la mejor opción.
- Documentar el propósito de las restricciones: Explicar por qué una clase es sealed y cuál es el modelo conceptual detrás de sus subclases permitidas.
- Utilizar non-sealed estratégicamente: El modificador
non-sealed
puede abrir partes específicas de la jerarquía cuando se necesita flexibilidad adicional.
sealed class Dispositivo permits DispositivoEntrada, DispositivoSalida, DispositivoCompuesto {
// Implementación común
}
sealed interface DispositivoEntrada permits Teclado, Raton, Joystick {}
sealed interface DispositivoSalida permits Monitor, Impresora, Altavoz {}
non-sealed class DispositivoCompuesto extends Dispositivo {
// Permite futuros dispositivos compuestos personalizados
}
- Aprovechar la verificación de exhaustividad: Diseñar el código para beneficiarse de la verificación de exhaustividad en las expresiones switch.
- Mantener la cohesión conceptual: Asegurarse de que todas las subclases permitidas representen variaciones del mismo concepto básico.
Limitaciones y consideraciones
- Las clases sealed no pueden prevenir la creación de instancias de las subclases permitidas desde cualquier parte del código con acceso a ellas.
- No son un sustituto para un buen diseño de seguridad si se trata de proteger información sensible.
- Si se necesitan añadir nuevas variantes con frecuencia, el enfoque sealed puede generar más cambios en el código existente que un enfoque abierto.
// Antes: sealed interface Moneda permits Euro, Dolar, Libra {}
// Después de añadir una nueva variante:
sealed interface Moneda permits Euro, Dolar, Libra, Yen {}
// Todos los switch sobre Moneda deberán actualizarse
- Las clases sealed funcionan mejor cuando se combinan con otras características de Java como records, pattern matching y expresiones switch.
Aprendizajes de esta lección
- Comprender la sintaxis y propósito de las clases sealed
- Aprender a usar la cláusula 'permits' para definir subclases permitidas
- Implementar pattern matching con clases sealed
- Aplicar buenas prácticas en el diseño y uso de clases sealed
- Mejorar la claridad del código mediante la control del flujo de extensibilidad
Completa Java y certifícate
Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.
Asistente IA
Resuelve dudas al instante
Ejercicios
Practica con proyectos reales
Certificados
Valida tus conocimientos
Más de 25.000 desarrolladores ya se han certificado con CertiDevs