El concepto de "clases e interfaces selladas" surge desde la versión Java 15 hasta la 17. Pero, ¿Qué son y para qué sirven las clases e interfaces sealed?
Estas construcciones permiten un control más estricto sobre la herencia (extends) y la implementación (implements), lo que mejora la seguridad, el diseño del código y la claridad en la intención del desarrollador.
- Clases sealed: Una clase sellada es una clase que restringe qué otras clases pueden extenderla. Esto se logra especificando explícitamente las subclases permitidas mediante la palabra clave permits.
- Interfaces sealed: Similar a las clases selladas, una interfaz sellada restringe qué clases o interfaces pueden implementarla o extenderla, también usando permits.
Ejemplos de clases sealed
public sealed class Vehiculo permits Coche, Moto { // Código de la clase } final class Coche extends Vehiculo { // Código de la clase } final class Moto extends Vehiculo { // Código de la clase } // Esto generaría un error de compilación class Camion extends Vehiculo { // No permitido, ya que no está en la lista de permits }
Ejemplos de interfaces sealed
public sealed interface Animal permits Perro, Gato { void hacerSonido(); } final class Perro implements Animal { public void hacerSonido() { System.out.println("Guau"); } } final class Gato implements Animal { public void hacerSonido() { System.out.println("Miau"); } } // Esto generaría un error de compilación class Elefante implements Animal { // No permitido, ya que no está en la lista de permits }
Las clases e interfaces selladas tienen varios usos y beneficios:
- Control estricto de la jerarquía de herencia: Permiten al desarrollador definir explícitamente qué clases pueden heredar de una clase o implementar una interfaz, evitando extensiones no deseadas. Esto es útil para diseñar APIs o bibliotecas donde se quiere limitar cómo los usuarios pueden extender el comportamiento.
- Mejora de la seguridad y robustez: Al restringir la herencia, se reduce el riesgo de que clases externas rompan invariantes o comportamientos esperados de la clase base. Por ejemplo, en sistemas críticos, esto puede prevenir errores o vulnerabilidades causados por extensiones no controladas.
- Facilitan el uso de pattern matching: Las clases selladas son especialmente útiles con el switch de pattern matching. Como el compilador sabe exactamente qué subclases existen, puede garantizar que todos los casos posibles estén cubiertos.
- Claridad en el diseño: Las clases e interfaces selladas hacen explícita la intención del diseño, mejorando la legibilidad y el mantenimiento del código. Los desarrolladores que usen tu código sabrán exactamente qué extensiones están permitidas, lo que reduce la ambigüedad.
Imaginemos que diseñamos un sistema de figuras geométricas:
public sealed abstract class Figura permits Circulo, Rectangulo { public abstract double calcularArea(); } final class Circulo extends Figura { private double radio; public Circulo(double radio) { this.radio = radio; } public double calcularArea() { return Math.PI * radio * radio; } } final class Rectangulo extends Figura { private double largo, ancho; public Rectangulo(double largo, double ancho) { this.largo = largo; this.ancho = ancho; } public double calcularArea() { return largo * ancho; } }
En este caso, solo Circulo y Rectangulo pueden extender Figura, lo que asegura que no se añadan figuras no deseadas al sistema.
Ejemplo. Crearemos una sealed interface con 4 clases record y la implementaremos, simulando éxitos y fallos de ejecución:
- Success si la operación tiene éxito.
- Failure si ocurre un error.
- Timeout si se excede el tiempo permitido.
- Interrupted si la operación es interrumpida.
sealed interface AsyncReturn<V> { record Success<V>(V result) implements AsyncReturn<V> {} record Failure<V>(Throwable cause) implements AsyncReturn<V> {} record Timeout<V>() implements AsyncReturn<V> {} record Interrupted<V>() implements AsyncReturn<V> {} }
Esta interface no será public, ya que se encontrará en la clase principal que la implementará:
TestSealedClassInterfaces.java
import java.util.concurrent.Future; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; // Definición de la interfaz sealed (como la proporcionaste) sealed interface AsyncReturn<V> { record Success<V>(V result) implements AsyncReturn<V> {} record Failure<V>(Throwable cause) implements AsyncReturn<V> {} record Timeout<V>() implements AsyncReturn<V> {} record Interrupted<V>() implements AsyncReturn<V> {} } public class TestSealedClassInterfaces { // Método que simula una operación asíncrona y devuelve un AsyncReturn public static <V> AsyncReturn<V> ejecutarOperacion(CompletableFuture<V> future, long timeoutMillis) { try { // Intentamos obtener el resultado con un tiempo límite V resultado = future.get(timeoutMillis, TimeUnit.MILLISECONDS); return new AsyncReturn.Success<>(resultado); } catch (java.util.concurrent.TimeoutException e) { return new AsyncReturn.Timeout<>(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // Restaurar el estado de interrupción return new AsyncReturn.Interrupted<>(); } catch (Exception e) { return new AsyncReturn.Failure<>(e); } } // Método para manejar el resultado de AsyncReturn public static <V> void manejarResultado(AsyncReturn<V> resultado) { switch (resultado) { case AsyncReturn.Success<V>(var valor) -> { System.out.println("Operación exitosa. Resultado: " + valor); } case AsyncReturn.Failure<V>(var causa) -> { System.out.println("Error en la operación: " + causa.getMessage()); } case AsyncReturn.Timeout<V> __ -> { System.out.println("La operación excedió el tiempo permitido."); } case AsyncReturn.Interrupted<V> __ -> { System.out.println("La operación fue interrumpida."); } } } public static void main(String[] args) { // Ejemplo 1: Operación exitosa CompletableFuture<String> futureExitoso = CompletableFuture.supplyAsync(() -> "¡Éxito!"); AsyncReturn<String> resultadoExitoso = ejecutarOperacion(futureExitoso, 1000); manejarResultado(resultadoExitoso); // Ejemplo 2: Operación con error CompletableFuture<String> futureFallido = CompletableFuture.supplyAsync(() -> { throw new RuntimeException("Algo salió mal"); }); AsyncReturn<String> resultadoFallido = ejecutarOperacion(futureFallido, 1000); manejarResultado(resultadoFallido); // Ejemplo 3: Operación con timeout CompletableFuture<String> futureLento = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(2000); // Simula una operación lenta } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "Demasiado tarde"; }); AsyncReturn<String> resultadoTimeout = ejecutarOperacion(futureLento, 500); manejarResultado(resultadoTimeout); } }
Salida:
$ java TestSealedClassInterfaces Operación exitosa. Resultado: ¡Éxito! Error en la operación: java.lang.RuntimeException: Algo salió mal La operación excedió el tiempo permitido.
Las clases e interfaces sealed en Java son herramientas poderosas para diseñar sistemas más seguros, claros y mantenibles. Son especialmente útiles en APIs, frameworks y sistemas donde la herencia debe ser estrictamente controlada, y se integran perfectamente con características modernas como el pattern matching.
Enlaces:
https://www.arquitecturajava.com/java-sealed-classes-y-jdk-17/https://javarevisited.blogspot.com/2022/02/how-to-use-sealed-classes-and-interfaces.html#axzz90QmSSReh
https://www.baeldung.com/java-sealed-classes-interfaces
Comentarios
Publicar un comentario