Classi e Interfacce Sealed in Java
- Le classi e interfacce sealed in Java, introdotte in JDK 17, consentono di controllare l'ereditarietà specificando quali classi o interfacce possono estenderle o implementarle.
- Una classe o interfaccia sealed è dichiarata con la parola chiave
sealed
e una clausolapermits
che elenca le classi o interfacce permesse. - Le sottoclassi di una classe sealed possono essere dichiarate come
final
,sealed
onon-sealed
. - Le classi e interfacce sealed sono utili per sviluppatori di librerie API, poiché permettono di limitare l'ereditarietà a un insieme specifico di classi e interfacce.
- Le classi sealed e le loro sottoclassi possono essere archiviate in un singolo file se hanno accesso al pacchetto predefinito, senza necessità di una clausola
permits
.
Classi e Interfacce sealed
A partire da JDK 17, è possibile dichiarare una classe che può essere ereditata solo da sottoclassi specifiche.
Tale classe è chiamata sealed o classe sigillata. Prima dell'avvento delle classi sealed, l'ereditarietà era una situazione tutto o niente. Una classe poteva essere estesa da qualsiasi sottoclasse oppure marcata come final
, il che impediva completamente che una classe potesse derivare da essa.
Le classi sealed
si collocano tra questi due estremi perché consentono di specificare precisamente quali sottoclassi una super-classe permetterà. In modo simile, è anche possibile dichiarare un'interfaccia sealed
in cui si specificano solo quelle classi che implementano l'interfaccia e/o quelle interfacce che estendono l'interfaccia sealed
. Insieme, le classi e le interfacce sealed
offrono un controllo significativamente maggiore sull'ereditarietà, il che può essere particolarmente importante quando si progettano librerie di classi.
Classi sealed
Per dichiarare una classe sealed, precediamo la sua dichiarazione con sealed
. Poi, dopo il nome della classe, includiamo una clausola permits
che specifica le sottoclassi consentite. Sia sealed
che permits
sono parole chiave sensibili al contesto che hanno significato speciale solo in una dichiarazione di classe o interfaccia. Al di fuori di una dichiarazione di classe o interfaccia, sealed
e permits
non vengono considerate come parole chiave e non hanno significato speciale. Ecco un semplice esempio di una classe sealed:
public sealed class MiaClasseSigillata
permits Alfa, Beta {
// ...
}
Qui, la classe sealed è chiamata MiaClasseSigillata
. Consente solo due sottoclassi: Alfa
e Beta
. Se qualsiasi altra classe tenta di ereditare MiaClasseSigillata
, si verificherà un errore in fase di compilazione.
Ecco Alfa
e Beta
, le due sottoclassi di MiaClasseSigillata
:
public final class Alfa extends MiaClasseSigillata {
// ...
}
public final class Beta extends MiaClasseSigillata {
// ...
}
Notiamo che ciascuna è specificata come final
. In generale, una sottoclasse di una classe sealed deve essere dichiarata come final
, sealed
, o non-sealed
. Vediamo ciascuna opzione. Prima, in questo esempio, ogni sottoclasse è dichiarata final
. Questo significa che le uniche sottoclassi di MiaClasseSigillata
sono Alfa
e Beta
, e non possono essere create sottoclassi di nessuna di queste. Pertanto, la catena di ereditarietà termina con Alfa
e Beta
.
Per indicare che una sottoclasse è essa stessa sealed, deve essere dichiarata sealed
e le sue sottoclassi permesse devono essere specificate. Ad esempio, questa versione di Alfa
permette Gamma
:
public sealed class Alfa extends MiaClasseSigillata permits Gamma {
// ...
}
Ovviamente, la classe Gamma
deve quindi essere anch'essa dichiarata sealed
, final
, o non-sealed
.
All'inizio potrebbe sembrare un po' sorprendente, ma possiamo rimuovere il sigillo da una sottoclasse di una classe sealed dichiarandola non-sealed
. Questa parola chiave sensibile al contesto è stata aggiunta da JDK 17. Sblocca la sottoclasse, consentendole di essere ereditata da qualsiasi altra classe. Ad esempio, Beta
potrebbe essere codificata così:
public non-sealed class Beta extends MiaClasseSigillata {
// ...
}
Ora, qualsiasi classe può ereditare Beta
. Tuttavia, le uniche sottoclassi dirette di MiaClasseSigillata
rimangono Alfa
e Beta
. Una ragione principale per usare non-sealed
è consentire a una super-classe di specificare un insieme limitato di sottoclassi dirette che forniscono una base di funzionalità ben definite ma permettono a quelle sottoclassi di essere liberamente estese.
Se una classe è specificata in una clausola permits
per una classe sealed, allora quella classe deve estendere direttamente la classe sealed. Altrimenti, risulterà un errore in fase di compilazione. Così, una classe sealed e le sue sottoclassi definiscono un'unità logica mutuamente dipendente. Inoltre, è illegale dichiarare una classe che non estende una classe sealed come non-sealed
.
Un requisito chiave di una classe sealed è che ogni sottoclasse che permette deve essere accessibile. Inoltre, se una classe sealed è contenuta in un modulo con nome, allora ogni sottoclasse deve essere anch'essa nello stesso modulo con nome. In questo caso, una sottoclasse può essere in un package diverso dalla classe sealed. Se la classe sealed è nel modulo senza nome, allora la classe sealed e tutte le sottoclassi permesse devono essere nello stesso package.
Nella discussione precedente, la super-classe MiaClasseSigillata
e le sue sottoclassi Alfa
e Beta
sarebbero state archiviate in file separati perché sono tutte classi pubbliche. Tuttavia, è anche possibile per una classe sealed e le sue sottoclassi essere archiviate in un singolo file (formalmente, un'unità di compilazione) purché le sottoclassi abbiano accesso package predefinito. In casi come questo, non è richiesta una clausola permits
per una classe sealed. Ad esempio, qui tutte e tre le classi sono nello stesso file:
// Poiché tutto questo è in un file,
// MiaClasseSigillata non richiede
// una clausola permits.
public sealed class MiaClasseSigillata {
// ...
}
final class Alfa extends MiaClasseSigillata {
// ...
}
final class Beta extends MiaClasseSigillata {
// ...
}
Un ultimo punto: Anche una classe astratta può essere sealed. Non c'è restrizione a questo riguardo.
Interfacce sealed
Un'interfaccia sealed è dichiarata nello stesso modo di una classe sealed, mediante l'uso di sealed
. Un'interfaccia sealed usa la sua clausola permits
per specificare le classi autorizzate ad implementarla e/o le interfacce autorizzate ad estenderla. Quindi, una classe che non fa parte della clausola permits
non può implementare un'interfaccia sealed, e un'interfaccia non inclusa nella clausola permits
non può estenderla.
Ecco un semplice esempio di un'interfaccia sealed che permette solo alle classi Alpha
e Beta
di implementarla:
public sealed interface MiaInterfacciaSigillata
permits Alpha, Beta {
void mioMetodo();
}
Una classe che implementa un'interfaccia sealed deve essa stessa essere specificata come final
, sealed
, o non-sealed
. Per esempio, qui Alpha
è marcata non-sealed
e Beta
è specificata come final
:
public non-sealed class Alpha implements MiaInterfacciaSigillata {
public void mioMetodo() {
System.out.println("Nel mioMetodo() di Alpha.");
}
// ...
}
public final class Beta implements MiaInterfacciaSigillata {
public void mioMetodo() {
System.out.println("All'interno del mioMetodo() di Beta.");
}
// ...
}
Ecco un punto chiave: Qualsiasi classe specificata nella clausola permits
di un'interfaccia sealed deve implementare l'interfaccia. Quindi, un'interfaccia sealed e le sue classi implementatrici formano un'unità logica.
Un'interfaccia sealed può anche specificare quali altre interfacce possono estendere l'interfaccia sealed. Per esempio, qui, MiaInterfacciaSigillata
specifica che MiaIF
è autorizzata ad estenderla:
// Si noti che MiaIF è aggiunta alla clausola permits.
public sealed interface MiaInterfacciaSigillata permits Alpha, Beta, MiaIF {
void mioMetodo();
}
Poiché MiaIF
fa parte della clausola permits
di MiaInterfacciaSigillata
, deve essere marcata come nonsealed
o sealed
e deve estendere MiaInterfacciaSigillata
. Per esempio,
public non-sealed interface MiaIF extends MiaInterfacciaSigillata {
// ...
}
Come ci si potrebbe aspettare, è possibile per una classe ereditare una classe sealed e implementare un'interfaccia sealed. Per esempio, qui Alpha
eredita MiaClasseSigillata
e implementa MiaInterfacciaSigillata
:
public non-sealed class Alpha
extends MiaClasseSigillata
implements MiaInterfacciaSigillata {
public void mioMetodo() {
System.out.println("Nel mioMetodo() di Alpha.");
}
// ...
}
Negli esempi precedenti, ogni classe e interfaccia sono dichiarate public
. Quindi, ognuna è nel suo proprio file. Tuttavia, come nel caso delle classi sigillate, è anche possibile per un'interfaccia sealed e le sue classi implementatrici (e interfacce estendenti) essere memorizzate in un singolo file purché le classi e le interfacce abbiano accesso al pacchetto predefinito. In casi come questo, nessuna clausola permits
è richiesta per un'interfaccia sealed. Per esempio, qui MiaInterfacciaSigillata
non include una clausola permits
perché Alpha
e Beta
sono dichiarate nello stesso file nel modulo senza nome:
public sealed interface MiaInterfacciaSigillata {
void mioMetodo();
}
non-sealed class Alpha
extends MiaClasseSigillata
implements MiaInterfacciaSigillata {
public void mioMetodo() {
System.out.println("Nel mioMetodo() di Alpha.");
}
// ...
}
final class Beta extends MiaClasseSigillata implements MiaInterfacciaSigillata {
public void mioMetodo() {
System.out.println("Nel mioMetodo() di Beta.");
}
// ...
}
Un punto finale: Le classi e interfacce sigillate sono più applicabili agli sviluppatori di librerie API in cui sottoclassi e sotto-interfacce devono essere strettamente controllate. Raramente esse sono usate in applicazioni generali. Tuttavia, quando sono utilizzate, le classi e interfacce sigillate possono essere molto utili per garantire che l'ereditarietà sia limitata a un insieme specifico di classi e interfacce.