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ícate

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

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.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

Plan mensual

19.00 € /mes

Precio normal mensual: 19 €
47 % DE DESCUENTO

Plan anual

10.00 € /mes

Ahorras 108 € al año
Precio normal anual: 120 €
Aprende Java online

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

Test

Listas

Código

Métodos de la clase String

Código

Streams: reduce()

Test

Polimorfismo

Código

Pattern Matching

Código

Streams: flatMap()

Test

Llamada y sobrecarga de funciones

Puzzle

Métodos referenciados

Test

Métodos de la clase String

Código

Representación de Fecha

Puzzle

Operadores lógicos

Test

Inferencia de tipos con var

Código

Tipos de datos

Código

Estructuras de iteración

Puzzle

Streams: forEach()

Test

Objetos

Puzzle

Funciones lambda

Test

Uso de Scanner

Puzzle

CRUD en Java de modelo Customer sobre un ArrayList

Proyecto

Tipos de variables

Puzzle

Streams: collect()

Puzzle

Operadores aritméticos

Puzzle

Arrays y matrices

Código

Clases y objetos

Código

Interfaz funcional Consumer

Test

Interfaces

Código

Enumeraciones Enums

Código

API java.nio 2

Puzzle

API Optional

Test

Interfaz funcional Function

Test

Encapsulación

Test

Interfaces

Código

Uso de API Optional

Puzzle

Representación de Hora

Test

Herencia básica

Test

Clases y objetos

Código

Interfaz funcional Supplier

Puzzle

HashMap

Puzzle

Sobrecarga de métodos

Test

Polimorfismo de tiempo de ejecución

Puzzle

OOP en Java

Proyecto

Sobrecarga de métodos

Código

Clases sealed

Código

Creación de Streams

Test

Records

Código

Encapsulación

Código

Streams: min max

Puzzle

Métodos avanzados de la clase String

Puzzle

Funciones

Código

Polimorfismo de tiempo de compilación

Test

Reto sintaxis Java

Proyecto

Conjuntos

Código

Estructuras de control

Código

Recursión

Código

Excepciones

Puzzle

Herencia avanzada

Puzzle

Estructuras de selección

Test

Uso de interfaces

Test

Operadores

Código

Variables

Código

HashSet

Test

Objeto Scanner

Test

Streams: filter()

Puzzle

Operaciones de Streams

Puzzle

Interfaz funcional Predicate

Puzzle

Streams: sorted()

Test

Configuración de entorno

Test

CRUD en Java de modelo Customer sobre un HashMap

Proyecto

Uso de variables

Test

Clases

Test

Streams: distinct()

Puzzle

Streams: count()

Test

ArrayList

Test

Datos de referencia

Test

Interfaces funcionales

Puzzle

Métodos básicos de la clase String

Test

Tipos de datos

Código

Clases abstractas

Código

Instalación

Test

Funciones

Código

Excepciones

Código

Estructuras de control

Código

Herencia de clases

Código

La clase Scanner

Código

Generics

Código

Streams: map()

Puzzle

Funciones y encapsulamiento

Test

Streams: match

Test

Gestión de errores y excepciones

Código

Datos primitivos

Puzzle

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

Accede GRATIS a Java y certifícate

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