Las clases sealed en Java nos ofrecen un control mucho más fino sobre la herencia que las clases finales o las clases normales. Mientras que una clase final no puede ser extendida por nadie, y una clase normal puede ser extendida por cualquier otra clase, una clase sealed solo puede ser extendida por aquellas clases que nosotros especifiquemos explícitamente. Esto nos permite diseñar jerarquías de clases más seguras y predecibles.
Para ilustrar esto, imaginemos que tenemos una clase llamada Desayuno. En un escenario normal, podríamos crear una clase Café que extienda de Desayuno sin problema alguno. Pero si marcamos Desayuno como final, entonces ninguna clase podrá extenderla, y el compilador nos lo impedirá. Sin embargo, si queremos permitir la extensión solo a ciertas clases, podemos usar la palabra clave sealed antes de class. Esto nos permite indicar que solo las clases que tengan permiso explícito podrán extender de Desayuno.
Para otorgar estos permisos, usamos la palabra clave permits seguida de los nombres de las clases autorizadas. Por ejemplo, si queremos que solo Café, Tostada y Croissant puedan extender Desayuno, declaramos:
public sealed class Desayuno permits Café, Tostada, Croissant {
// código común a todos los desayunos
}
Luego, cada una de estas clases debe estar marcada como final para evitar que alguien extienda de ellas y rompa la jerarquía controlada:
public final class Café extends Desayuno {
// implementación específica de Café
}
public final class Tostada extends Desayuno {
// implementación específica de Tostada
}
public final class Croissant extends Desayuno {
// implementación específica de Croissant
}
Es importante destacar que el modificador sealed solo controla quién puede extender o implementar una clase o interfaz, pero no afecta el comportamiento interno de la clase. Podemos seguir definiendo métodos y atributos como en cualquier otra clase.
Además, las clases sealed no solo se aplican a clases concretas, sino que también podemos usarlas con clases abstractas y con interfaces. Por ejemplo, podemos declarar una interfaz Nodo como sealed para controlar qué clases pueden implementarla:
public sealed interface Nodo permits NodoEstático, NodoDinámico {
String nombre();
String tipo();
}
Aquí, solo NodoEstático y NodoDinámico pueden implementar Nodo. La clase NodoEstático podría ser una clase final que implementa la interfaz:
public final class NodoEstático implements Nodo {
@Override
public String nombre() {
return "Nodo Estático";
}
@Override
public String tipo() {
return "Estático";
}
}
Por otro lado, los record en Java, que son clases inmutables y finales por naturaleza, también pueden participar en esta jerarquía sellada implementando interfaces sealed. Por ejemplo:
public record NodoDinámico(String nombre, String tipo) implements Nodo {
// Los métodos nombre() y tipo() se generan automáticamente
}
Como los record son finales, no necesitan ser marcados explícitamente como final.
Un detalle interesante es que no siempre es necesario usar la palabra clave permits si declaramos las clases derivadas dentro del mismo archivo o dentro de la misma clase o interfaz sellada. Por ejemplo, podemos definir la interfaz Nodo y sus implementaciones internas así:
public sealed interface Nodo permits NodoEstático, NodoDinámico {
String nombre();
String tipo();
public final class NodoEstático implements Nodo {
@Override
public String nombre() {
return "Nodo Estático";
}
@Override
public String tipo() {
return "Estático";
}
}
public record NodoDinámico(String nombre, String tipo) implements Nodo {
// Métodos generados automáticamente
}
}
En este caso, al estar las clases derivadas dentro de la interfaz sellada, el compilador entiende que solo estas clases pueden implementarla, y no es necesario usar permits.
Esta misma técnica puede aplicarse a clases selladas normales, como en el caso de Desayuno. Si definimos las clases derivadas dentro de la clase sellada, no necesitamos usar permits:
public sealed class Desayuno {
public final class Café extends Desayuno {
// implementación de Café
}
public final class Tostada extends Desayuno {
// implementación de Tostada
}
public final class Croissant extends Desayuno {
// implementación de Croissant
}
}
Así, el control de la herencia queda implícito y más claro, ya que todas las clases relacionadas están agrupadas.
En definitiva, las clases y interfaces sealed nos permiten diseñar jerarquías de tipos con un control estricto sobre quién puede extender o implementar, lo que resulta especialmente útil en programación orientada a datos y para mantener un diseño limpio y seguro. Además, la integración con record y la posibilidad de declarar las clases derivadas dentro de la clase o interfaz sellada facilitan la organización y el mantenimiento del código.