Ir al contenido principal

Java: sealed class & interfaces

 


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.

  1. 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
  2.  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

Entradas populares de este blog

Programación Windows Batch (CMD) parte 3

Crear ciclos para efectuar operaciones tediosas nos ahorrará tiempo para realizar otras tareas. En está ocasión veremos ciclos con FOR . ¿Cuál es la síntaxis de bucle FOR en Windows Batch? Si está dentro de un archivo *.bat : FOR %%variable IN (seq) DO operaciones Si lo ejecutamos en una terminal: FOR %variable IN (seq) DO operaciones Ejemplo 1 . Recorrer una secuencia de números del 0 al 5: recorrer.bat @ echo off FOR %%i in ( 0 1 2 3 4 5 ) DO echo Hola no. %%i pause Nos imprimirá en pantalla: Hola no. 0 Hola no. 1 Hola no. 2 Hola no. 3 Hola no. 4 Hola no. 5 ¿Puedo usar contadores? Si, se pueden usar. Ejemplo 2 . Uso de contadores: contador.bat @ echo off set numeros = 1 2 3 4 5 6 7 8 9 10 set cont = 0 for %%a in ( %numeros% ) do ( echo Hola no. %%a :: Contador set /a cont+ = 1 ) echo Total: %cont% Este código nos imprimirá, además de los mensajes Hola no. 0 ..., el total de valores conta...

Programación Windows Batch (CMD) parte 4

Siguiendo con la serie de post sobre programación ( 1 , 2 , y 3 ) batch ahora veremos algunas cosas como operaciones aritméticas, operadores lógicos  y uso de ficheros. Cuando somos administradores de servidores o desarrolladores muchas veces tenemos que realizar tareas que nos quitan, relativamente, tiempo valioso que podríamos ocupar para otras cosas (como ver nuestro Facebook, jeje, broma).  Aprender a escribir scripts que nos faciliten algunas tareas siempre es útil. Por ejemplo, conocer todas las características técnicas de nuestro equipo de cómputo nos servirá cuando se realiza peritajes informáticos y soporte al equipo. Realizar respaldos automáticos a nuestras carpetas , archivos y directorios será más sencillo gracias a un script. Pero antes debemos aprender lo básico de la programación en batch. Ejemplo 1. Operaciones aritméticas básicas. aritmetica.bat @ echo off ::Nombre del archivo, imprimirá: aritmetica.bat echo %0 :: Set nos servirá para a...

COBOL para principiantes #1

COBOL es un lenguaje de programación que sigue dando de que hablar. Los programadores Java, C#, Python, etc. saben que aún existen aplicaciones hechas en COBOL y es difícil que éstas migren a un lenguaje más actual. Es por esa y otras razones que muchos han pensado en aprender este lenguaje 'obsoleto'. ¡COBOL is the king, no ha muerto! ¡A desempolvar los libros de nuestros abuelos, tíos o maestros! ¿Qué debemos hacer para aprender COBOL y no morir en el intento? Para empezar necesitas: Tener bases de programación (obvio). Conseguir un compilador dependiendo del sistema operativo que uses (si usas Windows puedes usar Visual Studio e instalar un compilador; si usas Linux puedes usar Gnu OpenCOBOL, nosotros usaremos éste último en el blog ). Saber qué extensión se usa para crear un programa COBOL (.cb, cbl, .cb). Nosotros usaremos .cbl  Comprender la estructura de un programa COBOL.  Conocer las estructuras de control y estructuras de datos en COBOL. Practicar...