Al momento stai visualizzando Stranger things in Java: il modificatore protected

Stranger things in Java: il modificatore protected

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 package com.claudiodesio.blog.staj e importasse correttamente la classe ProtectedSubclassOtherPackage, 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 sottoclasse ProtectedSubclassOtherPackage, 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.

Lascia un commento