Clases sealed

Avanzado
Java
Java
Actualizado: 06/04/2025

¡Desbloquea el curso completo!

IA
Ejercicios
Certificado
Entrar

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áusula permits. 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.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

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

⭐⭐⭐⭐⭐
4.9/5 valoración