Java
Tutorial Java: Clases sealed
Java Clases Sealed: Aprende a gestionar la herencia con clases selladas, integrando pattern matching para un código seguro y claro.
Aprende Java y certifícateClases 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
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.
Ejercicios de esta lección Clases sealed
Evalúa tus conocimientos de esta lección Clases sealed con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Clases abstractas
Listas
Métodos de la clase String
Streams: reduce()
Polimorfismo
Pattern Matching
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Inferencia de tipos con var
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
CRUD en Java de modelo Customer sobre un ArrayList
Tipos de variables
Streams: collect()
Operadores aritméticos
Arrays y matrices
Clases y objetos
Interfaz funcional Consumer
Interfaces
Enumeraciones Enums
API java.nio 2
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Sobrecarga de métodos
Clases sealed
Creación de Streams
Records
Encapsulación
Streams: min max
Métodos avanzados de la clase String
Funciones
Polimorfismo de tiempo de compilación
Reto sintaxis Java
Conjuntos
Estructuras de control
Recursión
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
Operadores
Variables
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
CRUD en Java de modelo Customer sobre un HashMap
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Tipos de datos
Clases abstractas
Instalación
Funciones
Excepciones
Estructuras de control
Herencia de clases
La clase Scanner
Generics
Streams: map()
Funciones y encapsulamiento
Streams: match
Gestión de errores y excepciones
Datos primitivos
Todas las lecciones de Java
Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Instalación De Java
Introducción Y Entorno
Configuración De Entorno Java
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Recursión
Sintaxis
Excepciones
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Clases Abstractas
Programación Orientada A Objetos
Interfaces
Programación Orientada A Objetos
Sobrecarga De Métodos
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
La Clase Scanner
Programación Orientada A Objetos
Métodos De La Clase String
Programación Orientada A Objetos
Records
Programación Orientada A Objetos
Pattern Matching
Programación Orientada A Objetos
Inferencia De Tipos Con Var
Programación Orientada A Objetos
Enumeraciones Enums
Programación Orientada A Objetos
Generics
Programación Orientada A Objetos
Clases Sealed
Programación Orientada A Objetos
Listas
Framework Collections
Conjuntos
Framework Collections
Mapas
Framework Collections
Funciones Lambda
Programación Funcional
Interfaz Funcional Consumer
Programación Funcional
Interfaz Funcional Predicate
Programación Funcional
Interfaz Funcional Supplier
Programación Funcional
Interfaz Funcional Function
Programación Funcional
Métodos Referenciados
Programación Funcional
Creación De Streams
Programación Funcional
Operaciones Intermedias Con Streams: Map()
Programación Funcional
Operaciones Intermedias Con Streams: Filter()
Programación Funcional
Operaciones Intermedias Con Streams: Distinct()
Programación Funcional
Operaciones Finales Con Streams: Collect()
Programación Funcional
Operaciones Finales Con Streams: Min Max
Programación Funcional
Operaciones Intermedias Con Streams: Flatmap()
Programación Funcional
Operaciones Intermedias Con Streams: Sorted()
Programación Funcional
Operaciones Finales Con Streams: Reduce()
Programación Funcional
Operaciones Finales Con Streams: Foreach()
Programación Funcional
Operaciones Finales Con Streams: Count()
Programación Funcional
Operaciones Finales Con Streams: Match
Programación Funcional
Api Optional
Programación Funcional
Api Java.nio 2
Entrada Y Salida (Io)
Api Java.time
Api Java.time
Ecosistema Jakarta Ee De Java
Frameworks Para Java
Certificados de superación de Java
Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje 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