Al momento stai visualizzando Stranger things in Java: costanti

Stranger things in Java: costanti

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”.

In questo articolo, esploreremo alcune situazioni che coinvolgono l’utilizzo delle costanti in cui anche programmatori esperti potrebbero avere dei dubbi. Per quanto l’argomento potrebbe essere ben noto, non tutti hanno esplorato scenari particolari come quello risolvere l’ereditarietà multipla in presenza di costanti omonime. Rinforzare la propria base teorica è fondamentale per poter programmare con sicurezza.

 

Definizione

Si dice costante, una variabile che durante il suo ciclo di vita può assumere un unico valore. Questo significa che se nel nostro programma un’istruzione tentasse di cambiare il valore della costante, otterremo un errore in compilazione. Ciò che trasforma una variabile in costante in Java, è il modificatore final che viene specificato nella dichiarazione. Tale modificatore, ha proprio questo scopo, quello di limitare ad uno il numero delle assegnazioni possibili ad una variabile. Per esempio, se scriviamo:

final int CONST = 0;
CONST = 1;

otteniamo il seguente errore in compilazione:

error: cannot assign a value to final variable CONST
       CONST = 1;
       ^
1 error

perché dopo che abbiamo assegnato un valore alla variabile, non è possibile assegnarne un altro con l’istruzione:

CONST =  1;

Ecco perché la convenzione per i nomi delle costanti richiede che si usino solo lettere maiuscole (ed il carattere underscore “_” in caso di nomi composti da più parole). Questa particolare convenzione infatti, ci garantisce di poter distinguere subito una costante da una variabile. In questo modo sarà più difficile incorrere in un errore come il precedente.

 

Categorizzazione delle costanti

Come siamo abituati a categorizzare le variabili in variabili d’istanza, variabili di classe e variabili locali, dobbiamo anche distinguere le costanti di classe, variabili d’istanza e le costanti locali. In particolare nello snippet precedente abbiamo definito una costante locale.

Sono da considerarsi costanti locali, anche alcuni casi particolari come quello dei parametri costanti dei metodi (vedi sezione dedicata più sotto), o le costanti definite contestualmente alla dichiarazione di alcuni costrutti di programmazione. Per esempio, potremmo scrivere un ciclo foreach dichiarando la variabile temporanea come una costante nel seguente modo:

for (final var tmp : arr) {
    System.out.println(tmp);
}

Alcuni sviluppatori ritengono utile questa pratica per sottolineare che la variabile temporanea non deve essere modificata all’interno del blocco di codice del costrutto. Notare che, il fatto di poter dichiarare la variabile tmp come costante, ci fa capire che tale variabile in realtà è locale non rispetto al costrutto ciclo, ma rispetto all’iterazione corrente del ciclo. Se non fosse così il modificatore final impedirebbe la riassegnazione di un valore (quindi il ciclo precedente non potrebbe essere compilato).

Questo comportamento è diverso rispetto a quello della variabile che di solito inizializziamo
in un ciclo for, che ha visibilità a livello di ciclo e non di iterazione. Infatti il seguente snippet

for (final int i = 0; i < 10; i++) {
//…
}

produrrà l’errore:

error: cannot assign a value to final variable i
        for (final int i = 0; i < 10; i++) {
                                      ^
1 error

Dopo questa osservazione, passiamo a vedere degli scenari più interessanti.

 

Costanti locali

Le costanti locali sono oggi relativamente poco utilizzate, ma non è stato sempre così. Infatti, esiste anche uno stile di programmazione che richiede l’uso del modificatore final per ogni variabili locale a cui viene assegnato un unico valore durante il suo ciclo di vita. Infatti, dichiarando final una variabile il cui valore non sarà modificato, mette subito in chiaro la logica con la quale si è definita: il suo valore non deve cambiare. È un modo per mettere un vincolo nel nostro codice che deve essere rispettato anche da altri programmatori che lo modificheranno in futuro. Questo stile di programmazione è coerente con i principi di progettazione object oriented ed è considerato da alcuni una best practice. Per esempio, quando con alcuni IDE come Eclipse si esegue un refactoring di tipo “extract local variable” per assegnare il valore di ritorno di un metodo ad una variabile creata automaticamente, di default viene aggiunto a tale variabile anche il modificatore final.

Figura 1: La dialog di Eclipse che aggiunge final quando “estrae un variabile locale”

Tuttavia Java è noto per essere un linguaggio tanto espressivo quanto verboso. Per tale ragione, la tendenza ad aggiungere final alle variabili che non vengono modificate è oggi relativamente poco utilizzata. Oracle stessa, che tanto si sta impegnando a ridurre la proverbiale verbosità di Java, introducendo il concetto di variabile effectively final (ovvero effettivamente costante) nella versione 8, ha dato anche una chiara indicazione: i programmatori preferiscono uno stile più meno verboso. Infatti, prima di Java 8 per utilizzare una variabile locale all’interno di una classe innestata locale (e successivamente nelle espressioni lambda) era obbligatorio dichiararla final, mentre ora basta che la variabile sia inizializzata una sola volta.

 

Costanti parametro

Anche i parametri di un metodo possono essere dichiarati final. Per esempio possiamo dichiarare final il parametro del metodo main per prevenirne la riassegnazione. Per esempio:

public static void main(final String args[]) {
    args = new String[5];
//. . .

produrrà il seguente errore in compilazione:

error: final parameter args may not be assigned
        args = new String[5];
        ^
1 error

Anche nel caso dei parametri dei metodi, l’utilizzo di final viene solitamente evitato, ma in alcune situazioni può risultare utile. La ragione è sempre la stessa, quella di rendere il codice più espressivo aumentando però la verbosità.

 

Costanti di classe

La maggior parte delle volte però facciamo uso delle costanti in quanto campi delle nostre classi. In particolare siamo soliti inizializzare una costante contestualmente alla propria dichiarazione, per esempio nel seguente modo:

public class FileManager {
    private final static String FILE_NAME = "aFile.java";
    
    //. . . 
}

Notare che in questo caso abbiamo dichiarato la costante anche statica, e questa può considerarsi una best practice. In questo modo infatti, il modificatore static farà sì che esista un’unica copia della costante per quella classe, che sarà condivisa da tutti gli oggetti istanziati. Questo eviterà che al runtime per ogni oggetto istanziato vengano create in memoria copie identiche della costante. Una costante infatti ha un valore fisso, quindi non è rischioso che tutti gli oggetti della stessa classe la condividano perché che il suo valore non può cambiare. Le costanti dichiarate final e static vengono dette costanti di classe.

Ovviamente, se vogliamo che ogni oggetto istanziato abbia un valore diverso per la propria costante, allora il modificatore static non si deve utilizzare.

 

Costanti di istanza

Abbiamo detto che solitamente siamo abituati a inizializzare una costante contestualmente alla propria dichiarazione. Ma questo non è obbligatorio, anzi è possibile anche inizializzare una costante d’istanza all’interno di un costruttore. La seguente classe per esempio:

public class FileManager {
    private final String FILE_NAME;
    
    public FileManager(String fileName) {
        FILE_NAME = fileName;
    }
}

ha un costruttore che assegna il valore alla costante FILE_NAME tramite un parametro che proviene dall’esterno. Questo permette di assegnare per ogni oggetto un valore diverso alla costante FILE_NAME. Per esempio possiamo scrivere:

FileManager readmeFM = new FileManager("readme.txt");
FileManager licenseFM = new FileManager("license.txt");

In questo modo, i due oggetti avranno la costante FILE_NAME inizializzata in maniera differente.

Ovviamente se avessimo dichiarato la costante FILE_NAME anche statica, questo non sarebbe stato possibile. Attenzione che in quel caso il compilatore avrebbe dato un messaggio di errore fuorviante:

error: cannot assign a value to final variable FILE_NAME
        FILE_NAME = fileName;
        ^
1 error

Infatti sembra che ci indichi che il problema sia il modificatore final, ma in realtà il problema deriva dall’uso del modificatore static.

 

Costanti d’istanza e metodi

Notare che non è possibile settare il valore di una costante di istanza all’interno di un metodo che non sia un costruttore. Per esempio, se scrivessimo:

public class FileManager {
    private final String FILE_NAME;
    
    public FileManager(String fileName) {
        setFILE_NAME(fileName);
    }
    
    public void setFILE_NAME(String fileName) {
        FILE_NAME = fileName;
    }
}

otterremmo il seguente errore in compilazione:

error: cannot assign a value to final variable FILE_NAME
        FILE_NAME = fileName;
        ^
1 error

Questo perché, a differenza di un costruttore, il metodo setFILE_NAME potrebbe essere chiamato al runtime più di una volta, causando la violazione del contratto definito dal modificatore final.

 

Costanti d’istanza ed overload di costruttori

Notare anche che nel caso di overload di costruttori, il compilatore sarà capace di riconoscere se la costante viene inizializzata correttamente. Per esempio, se aggiungessimo alla classe FileManager un costruttore senza istruzioni nel seguente modo:

public class FileManager {
    private final String FILE_NAME;
    
    public FileManager() {
    }
       
    public FileManager(String fileName) {
        FILE_NAME = fileName;
    }
}

il compilatore comprenderà che se chiamiamo il secondo costruttore, la costante FILE_NAME non verrà inizializzata e quindi ci presenterà questo messaggio di errore:

error: variable FILE_NAME might not have been initialized
    }     
    ^
1 error

Se invece con il primo costruttore chiamiamo mediante la parola chiave this il secondo costruttore passandogli il nome di un file di default, allora il programma tornerà a poter essere compilato correttamente:

public class FileManager {
    private final String FILE_NAME;
    
    public FileManager() {
        this("defaultFile.txt");
    }
       
    public FileManager(String fileName) {
        FILE_NAME = fileName;
    }
}

Infatti, non ci sarà possibilità di settare più volte FILE_NAME.

 

Costanti ed incapsulamento

È probabile che le nostre costanti d’istanza o di classe siano dichiarate anche pubbliche.

Infatti, anche se siamo abituati ad incapsulare le nostre variabili per evitare che assumano valori indesiderati, non ha senso incapsulare le nostre costanti, in quanto esse non potranno mai assumere un valore indesiderato.

Nella libreria standard di Java possiamo trovare tantissime costanti statiche e pubbliche, come per esempio le costanti PI ed E della classe Math, o MAX_PRIORITY, NORM_PRIORITY e MIN_PRIORITY della classe Thread.

 

Costanti, polimorfismo ed ereditarietà

Per le regole del polimorfismo, sappiamo che se invochiamo un metodo di un oggetto utilizzando un reference di una superclasse, verrà invocato il metodo riscritto della classe con cui è stato istanziato l’oggetto, e non il metodo della classe del reference che stiamo utilizzando. Infatti, se consideriamo questa semplice gerarchia di classi:

abstract class Pet {
    public final String type = "Generic Pet";
    
    public abstract void talk();
}

class Dog extends Pet {
    public final String type = "Dog"; 

    public void talk() {
        Ststem.out.println("Woof woof!")
    }
}

con il seguente snippet:

Pet bobby = new Dog();
bobby.talk();

invocheremo il metodo talk ridefinito nella classe Dog e non il metodo originale della classe Pet (che tra l’altro era astratto).

La situazione cambia nel caso proviamo ad accedere a variabili o costanti pubbliche (sia statiche che non statiche), che andiamo a sovrascrivere nelle sottoclassi. Infatti non esiste override per gli attributi di una classe, e quindi le regole sono diverse. Per esempio, il seguente codice:

Pet snoopy = new Dog();
System.out.println(snoopy.name);
Dog punky = new Dog();
System.out.println(punky.name);        

darà luogo al seguente output:

Generic Pet
Dog

il che implica, che anche se la costante name è stata riscritta nella sottoclasse Dog, per accedervi c’è bisogno di un reference della stessa classe. Infatti, usando il reference della superclasse Pet si accede alla costante della classe Pet.

 

Costanti ed ereditarietà multipla

Dalla versione 8 di Java, con l’introduzione dei metodi di default nelle interfacce, è possibile utilizzare una nuova sorta di ereditarietà multipla. Non si tratta della stessa complessa caratteristica definita in alcuni linguaggi come il C++, ma di una semplice conseguenza dell’evoluzione del concetto di interfaccia. Le regole che governano l’ereditarietà multipla di Java sono molto semplici, e l’unica che può destare qualche dubbio è nota come “class always wins”. In pratica, se ereditiamo due metodi con la stessa firma da una classe e da un’interfaccia, verrà ereditato sempre quello della classe (la classe vince sempre). In tutti gli altri casi di omonimia di metodi ereditati, il compilatore ci costringe a fare override del metodo.

Detto questo, sappiamo che le interfacce non possono dichiarare variabili, ma possono dichiarare costanti statiche e pubbliche. Infatti che non c’è neanche l’obbligo di contrassegnarle con i modificatori public, static, e final, che sono impliciti nelle interfacce. Quindi come funziona se ereditiamo da due tipi diversi un costante con lo stesso nome? La risposta è che il compilatore ci obbligherà sempre a riscrivere la costante. Per esempio. consideriamo il seguente codice:

abstract class AbstractClass {
    public static final int VALUE = 1;
}
interface Interface {
    int VALUE = 2;
}
class Subclass extends AbstractClass implements Interface {
    public static void main(String args[]) {
        System.out.println(VALUE);
    }
}

Se provassimo a compilare il file contenente il codice precedente, otterremmo il seguente errore in compilazione:

error: reference to VALUE is ambiguous
        System.out.println(VALUE);
                           ^
  both variable VALUE in AbstractClass and variable VALUE in Interface match
1 error

Quindi per le costanti la regola “class always wins” non vale. In particolare, nell’atto di riscrittura della costante bisogna sempre referenziare la classe a cui appartiene, per esempio:

System.out.println(AbstractClass.VALUE);

 

Conclusioni

In questo articolo, abbiamo approfondito alcuni aspetti dell’utilizzo delle costanti, un argomento base del linguaggio che alcune volte viene utilizzato con sufficienza. Abbiamo invece visto che ci sono situazioni dove le costanti hanno un comportamento singolare. Come al solito, avere una solida base teorica ci permetterà di gestire tutte le situazioni senza sorprese.

 

Note dell’autore

Questo articolo è basto su alcuni paragrafi del mio libro “Il nuovo Java”, e del mio libro in inglese “Java for Aliens”.

Lascia un commento