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 possa essere considerato ben noto ai più, non tutti hanno esplorato alcuni scenari particolari come quello in cui l'ereditarietà multipla coinvolge 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 v ariabile 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
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, costanti 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 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 detto 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() { System.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 codice:Pet snoopy = new Dog(); System.out.println(snoopy.type); Dog punky = new Dog(); System.out.println(punky.type);
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 da una classe
e da un'interfacciadue metodi con la stessa firma, 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 una 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.
