Questo articolo fa parte della serie “Stranger things in Java”, dedicata agli approfondimenti del linguaggio che ci permetteranno di padroneggiare anche gli scenari più strani che si possono presentare quando programmiamo. Tutti gli articoli sono ispirati dal contenuto dal libro “Java for Aliens” (in inglese) e dal libro “Il nuovo Java”.
Il modificatore protected
è un specificatore d’accesso utilizzato alquanto raramente, applicabile a variabili, metodi e costruttori, ma non a tipi Java (classi, interfacce, enumerazioni, annotazioni e record), a meno che essi non siano innestati in altri tipi. In questo articolo eviteremo di parlare dei tipi innestati e ci limiteremo a parlare di tale modificatore riferendoci soprattutto ai membri (variabili e metodi) delle classi per semplificare il discorso. In particolare faremo alcune osservazioni importanti riguardo tale modificatore, che è spesso usato senza la necessaria consapevolezza. Inoltre faremo una digressione anche sul pattern Singleton per aggiungere ad esso l’estensibilità.
Definizione
Tutti i programmatori Java conoscono le proprietà dei modificatori public
e private
, giacché utilizzati quotidianamente nella programmazione. Il modificatore protected
invece, viene usato molto meno frequentemente, e quando viene impiegato non è raro che si utilizzi in maniera inopportuna, a volte perché se ne si ignora la corretta definizione.
Quando applicato ad un membro di una classe, il modificatore protected
definisce il grado di accessibilità più alto dopo quello definito dal modificatore public
. Un membro protetto infatti, sarà accessibile all’interno dello stesso package (tramite l’operatore dot), ma verrà anche ereditato in tutte le sottoclassi della classe in cui è definito, anche se non appartenenti allo stesso package.
Visibilità di package
In termini di visibilità di un membro quindi, usare il modificatore protected
o non usare alcun modificatore è la stessa cosa. Infatti, quando non anteponiamo modificatori ad un membro di una classe, si parla di visibilità di package (o visibilità di default), e tale membro risulterà accessibile in tutti i tipi definiti nello stesso package.
Per esempio consideriamo la classe ProtectedClass
che dichiara una variabile e un metodo protetti:
package com.claudiodesio.blog.staj; public class ProtectedClass { protected int protectedVariable; protected void protectedMethod() { System.out.println("Protected method invoked"); } }
Inoltre consideriamo la seguente classe ProtectedClassSamePackage
appartenente allo stesso package:
package com.claudiodesio.blog.staj; public class ProtectedClassSamePackage { public void methodUsingProtectedMembers() { var protectedClass = new ProtectedClass(); protectedClass.protectedMethod(); System.out.println(protectedClass.protectedVariable); } }
Tale classe compilerà senza problemi, anche se utilizza esplicitamente i membri dichiarati protetti nella superclasse tramite l’operatore dot come se fossero dichiarati pubblici.
Invece, se proviamo ad utilizzare i membri protected
allo stesso modo in una sottoclasse appartenente ad un diverso package come la seguente:
package com.claudiodesio.blog.staj.other; import com.claudiodesio.blog.staj.ProtectedClass; public class ProtectedClassOtherPackage { public void methodUsingProtectedMembers() { var protectedClass = new ProtectedClass(); protectedClass.protectedMethod(); System.out.println(protectedClass.protectedVariable); } }
otterremo i seguenti errori in compilazione:
ProtectedClassOtherPackage.java:9: error: protectedMethod() has protected access in ProtectedClass protectedClass.protectedMethod(); ^ ProtectedClassOtherPackage.java:10: error: protectedVariable has protected access in ProtectedClass System.out.println(protectedClass.protectedVariable); ^ ProtectedClassOtherPackage.java uses preview language features. 2 errors
proprio perché ci troviamo in un package differente.
Avremmo ottenuto lo stesso risultato anche se i membri della superclasse li avessimo dichiarati con visibilità di package, ovvero non avessimo utilizzato nessun modificatore.
Ereditarietà
Dichiarare un membro protected
però, è importante quando oltre a voler limitare la visibilità al package di appartenenza, vogliamo anche che esso sia ereditato nelle sottoclassi anche se esterne al package. Utilizzare protected
quindi, ha senso solo quando vogliamo utilizzare l’ereditarietà. Infatti le sottoclassi ereditano membri dichiarati protetti nella superclasse, anche se tali sottoclassi appartengono a package differenti dal package a cui appartiene la superclasse.
Per esempio la seguente sottoclasse della classe ProtectedClass
:
package com.claudiodesio.blog.staj; public class ProtectedSubclassSamePackage extends ProtectedClass { public void methodUsingProtectedMembers() { var protectedClass = new ProtectedClass(); protectedMethod(); System.out.println(protectedVariable); } }
appartiene allo stesso package della superclasse, e viene compilata senza errori.
Ma lo stesso discorso vale anche per una sottoclasse che appartiene ad un package diverso come la seguente:
package com.claudiodesio.blog.staj.other; import com.claudiodesio.blog.staj.ProtectedClass; public class ProtectedSubclassOtherPackage extends ProtectedClass { public void methodUsingProtectedMembers() { var protectedClass = new ProtectedClass(); protectedMethod(); System.out.println(protectedVariable); } }
The stranger thing
Purtroppo, un’altra classe che si trova in un package diverso da com.claudiodesio.blog.staj
, non potrà accedere ai membri protetti ereditati dalla superclasse ProtectedClass
, perché la visibilità di tali metodi è circoscritta al package dove sono stati dichiarati. Per esempio, la compilazione della seguente classe:
package com.claudiodesio.blog.staj.other; public class StrangerThingsAboutProtectedTest { public static void main(String args[]) { ProtectedSubclassOtherPackage psop = new ProtectedSubclassOtherPackage(); psop.metodoProtected(); } }
darà luogo ad un errore la cui descrizione non lascia dubbi:
StrangerThingsAboutProtectedTest.java:7: error: protectedMethod() has protected access in ProtectedClass psop.protectedMethod(); ^ 1 error
Quindi, nonostante StrangerThingsAboutProtectedTest
si trovi nello stesso package della classe ProtectedSubclassOtherPackage
, non può invocarne il metodo protetto, perché la visibilità di tale metodo è limitata al package com.claudiodesio.blog.staj
dove è stato dichiarato.
Chiaramente, se la classe
StrangerThingsAboutProtectedTest
appartenesse al packagecom.claudiodesio.blog.staj
e importasse correttamente la classeProtectedSubclassOtherPackage
, la compilazione sarebbe andata a buon fine.
Workaround
In questo caso, per risolvere il problema possiamo fare override del metodo protetto protectedMethod
nella sottoclasse ProtectedSubclassOtherPackage
, per esempio nel seguente modo:
package com.claudiodesio.blog.staj.other; import com.claudiodesio.blog.staj.ProtectedClass; public class ProtectedSubclassOtherPackage extends ProtectedClass { public void methodUsingProtectedMembers() { var protectedClass = new ProtectedClass(); protectedMethod(); System.out.println(protectedVariable); } @Override protected void protectedMethod() { super.protectedMethod(); } }
In questo modo la classe StrangerThingsAboutProtectedTest
si può compilare ed eseguire correttamente. Infatti, il metodo protectedMethod
è ridefinito dalla classe ProtectedSubclassOtherPackage
, appartenente al package com.claudiodesio.blog.staj.other
dove si trova anche la classe StrangerThingsAboutProtectedTest
.
Notare che per le regole dell’override, il metodo
protectedMethod
nella sottoclasseProtectedSubclassOtherPackage
, portebbe anche essere dichiarato public.
Variabili protected
Lo stesso problema si pone anche con le variabili protette. Infatti, se il metodo della classe StrangerThingsAboutProtectedTest
provasse ad accedere alla variabile protectedVariable
:
public static void main(String args[]) { ProtectedSubclassOtherPackage psop = new ProtectedSubclassOtherPackage(); psop.protectedVariable = 1; }
otterremmo il seguente errore:
StrangerThingsAboutProtectedTest.java:8: error: protectedVariable has protected access in ProtectedClass psop.protectedVariable = 1; ^ 1 error
Anche in questo caso, per poter compilare correttamente, potremmo riscrivere la variabile nella sottoclasse:
// dichiarazione di package e di import omessi public class ProtectedSubclassOtherPackage extends ProtectedClass { protected int protectedVariable; // resto del codice omesso
Una soluzione assolutamente non elegante. D’altronde, come vedremo nelle prossime sezioni, il modificatore protected
può essere molto utile quando applicato a metodi, a costruttori e in alcuni casi a costanti. È molto meno utile quando applicato alle variabili.
Costruttori protected
Dichiarando un costruttore protected
, possiamo rendere una classe istanziabile solo all’interno di un certo package, ma contemporaneamente estendibile con sottoclassi appartenenti anche a package diversi. Un esempio interessante è quello relativo al famoso design pattern Singleton. Una sua tipica implementazione in Java, prevede la dichiarazione del costruttore privato. In realtà, nello storico libro che per primo ha formalizzato questo pattern: “Design Patterns: Elements of Reusable Object-Oriented Software”, si proponeva di dichiarare il costruttore protected
, per permettere l’estensione della classe Singleton (vedi figura 1).
Figura 1: l’esempio di codice scritto in C++ proposto nel libro Design Patterns.
L’esempio proposto era scritto in C++ (Java all’epoca non esisteva ancora), dove il modificatore protected
è definito in maniera più semplice e naturale. Non esistendo il concetto di package infatti, l’utilizzo di protected
in C++ garantisce una visibilità privata ed il supporto all’ereditarietà.
Per supportare l’ereditarietà di una classe Singleton in Java invece, oltre a dichiarare il costruttore protected
, occorre anche includere tale classe all’interno di un package dedicato, ovvero che non contiene altre classi. Se così non fosse, le altre classi potrebbero istanziare la classe singleton violando la filosofia del pattern. Tenendo presente quanto appena detto, il seguente esempio rappresenta un singleton estendibile:
package com.claudiodesio.blog.staj.singleton; public class SingletonExample { private static SingletonExample instance; protected SingletonExample () { } public static SingletonExample getInstance() { if (instance == null) { synchronized (SingletonExample.class) { instance = new SingletonExample(); } } return instance; } //altro codice omesso }
A patto che il package com.claudiodesio.blog.staj.singleton
non contenga altre classi.
Variabili protected
Ma se l’utilizzo di protected
è destinato soprattutto a metodi e costruttori, dichiarare una variabile protected
, la maggior parte delle volte non è necessario o addirittura sbagliato.
Un errore comune tra i neofiti infatti, è quello di pensare che per poter utilizzare una certa variabile in una sottoclasse, sia necessario dichiararla protected
, quando è già sufficiente avere a disposizione i metodi mutator (set) ed accessor (get) nelle sottoclassi. Inoltre, dichiarare protetta una variabile d’istanza significa renderla pubblica a tutte le classi incluse nello stesso package. Questo significa che la variabile protected
è accessibile direttamente a tutte le classi appartenenti allo stesso package, e tranne in rari casi non è questo il livello di incapsulamento desiderato. Quindi
se consideriamo la classe Articolo
:
public class Articolo { private int id; public void setId(int id) { this.id = id; } public int getId() { return id; } }
Nella sua sottoclasse Libro
, possiamo settare e leggere tranquillamente la variabile id. Infatti, anche non avendola ereditata, abbiamo comunque ereditato i metodi setId
e getId
. Per esempio potremmo implementare la sottoclasse Libro
nel seguente modo:
public class Libro extends Articolo { public Libro (int id, String titolo){ setId(id); setTitolo(titolo); } private String titolo; public void setTitolo(String titolo) { this.titolo = titolo; } public String getTitolo() { return titolo; } }
Notare che abbiamo usato il metodo setId
all’interno del costruttore. Quindi non è necessario dichiarare nella superclasse Articolo
la variabile id come protected
, e rompere così il contratto dell’incapsulamento.
Conclusioni
In questo breve articolo abbiamo approfondito un argomento di base, come il modificatore protected
, ed in particolare il suo rapporto con l’ereditarietà. Tale modificatore può essere utile se applicato a metodi e costruttori, molto meno per le variabili incapsulate. Siccome è uno di quegli argomenti che vengono imparati nelle prime fasi di apprendimento del linguaggio, e trattandosi di un modificatore di uso abbastanza raro, alcuni programmatori ne trascurano le sottigliezze della sua definizione, ed in alcuni casi viene utilizzato in maniera inopportuna.
Note dell’autore
Questo articolo è basto su alcuni paragrafi dei capitoli 5 e 6 del libro “Il nuovo Java” e dei capitoli 6 e 7 del mio libro in inglese “Java for Aliens”. Il libro citato all’interno dell’articolo “Design Patterns: Elements of Reusable Object-Oriented Software” di Erich Gamma, John Vlissides, Richard Helm, Ralph Johnson meglio noti come Gang of Four o più brevemente GoF, è il primo libro che ha formalizzato il concetto di design pattern nell’informatica.