Secondo alcuni sondaggi come quello di JetBrains, la versione 8 di Java è al momento quella più utilizzata in assoluto dagli sviluppatori di tutto il mondo, nonostante si tratti di una release del 2014.
Quello che state leggendo è il primo di una serie di articoli intitolata “Superare Java 8”, ispirata ai contenuti del mio ultimo libro “Il nuovo Java”. Questi articoli accompagneranno passo dopo passo il lettore all'esplorazione delle più importanti caratteristiche introdotte a partire dalla versione 9. L'obiettivo è quello di far acquisire la consapevolezza di quanto è importante aggiornare le proprie conoscenze relative a Java, spiegando gli enormi vantaggi che offrono le ultime versioni del linguaggio.
In questo articolo parleremo della più importante novità introdotta con Java 10.
Ufficialmente chiamata deduzione del tipo per le
variabili locali (in inglese local variable type inference),
questa caratteristica è meglio nota come introduzione della parola
var
. A dispetto del nome complicato, in realtà si tratta di una
caratteristica piuttosto semplice da utilizzare. Tuttavia bisogna fare diverse
osservazioni per vedere l'impatto che ha l'introduzione della parola var
su
altre caratteristiche preesistenti.
Verbosità
Negli ultimi anni, una parte importante dell'evoluzione del linguaggio Java, è stata
dedicata a rendere la sintassi più sintetica (nella programmazione solitamente si
preferisce dire meno verbosa). Per fare ciò, si
è soprattutto cercato di assegnare ulteriori compiti al compilatore. Nel caso della
deduzione del tipo per le variabili locali, il compilatore riesce a dedurre
automaticamente il tipo della variabile locale che stiamo dichiarando, permettendoci di
utilizzare la parola var
, in luogo del tipo della variabile. Per esempio,
supponiamo di avere la classe LibroSuJava
, sappiamo istanziarne un oggetto
con la seguente sintassi:
LibroSuJava ogg1 = new LibroSuJava();
Sfruttando la deduzione del tipo per le variabili locali, possiamo invece scrivere:
var ogg1 = new LibroSuJava();
Il compilatore infatti, è capace di dedurre il tipo della variabile ogg1
analizzando la sua inizializzazione, ovvero la parte destra
della dichiarazione (in inglese
right hand side che viene spesso abbreviato in
RHS). Il vantaggio immediato, è quello di potere
avere un codice meno verboso, rischiando di sbagliare di meno e senza comprometterne la
leggibilità.
Leggibilità
Non sempre però l'utilizzo della parola var
, non peggiorerà la leggibilità
del nostro codice. Nell'esempio precedente infatti, si parla di
tipo manifesto (in inglese manifest type), perché leggendo la dichiarazione
della variabile ogg1
, il tipo dedotto è evidente anche a noi programmatori.
Basta leggere la parte destra della dichiarazione invece della parte sinistra.
La deduzione del tipo però, avrebbe luogo anche nel caso in cui, invece di assegnare
manifestamente alla variabile ogg1
un'istanza della classe
LibroSuJava
, le assegnassimo il valore di ritorno di un metodo. Per
esempio, supponiamo di avere a disposizione nella stessa classe il seguente metodo:
public LibroSuJava getIstanza() { return new LibroSuJava(); }
potremmo comunque sfruttare la deduzione del tipo in questo modo:
var ogg2 = getIstanza();
In questo caso si pone un problema di leggibilità per noi programmatori. Infatti,
benché il compilatore deduca automaticamente il tipo per ogg2
, noi saremo
costretti a leggere il tipo di ritorno nella dichiarazione del metodo
getIstanza
per scoprire che si tratta di un tipo LibroSuJava
.
Per tale ragione si parla di tipo non manifesto.
In questo specifico caso, abbiamo migliorato la verbosità del nostro codice a discapito
della leggibilità, e questa non sembra un buona decisione. Bisognerebbe sempre favorire
la manutenzione dei nostri programmi piuttosto che risparmiare qualche digitazione sulla
tastiera.
Best practice
Sappiamo tutti che assegnare nomi significativi alle nostre variabili è molto
importante nella programmazione Java. Nel caso di utilizzo di un tipo non manifesto,
scegliere un nome significativo della variabile diventa ancora più importante.
L'identificatore ogg2
per esempio, non sembra essere adatto visto che non
ci dà informazioni sul suo tipo. In questo particolare caso potremmo invece dichiarare
la variabile nel seguente modo:
var libroSuJava = getIstanza();
In questo modo potremmo supporre ragionevolmente il tipo della variabile. È fondamentale
quindi utilizzare nomi significativi per le variabili che vogliamo dichiarare tramite
la parola var
.
Parola chiave?
La parola var
, non è una parola chiave, ma un nome di tipo riservato. Una parola chiave non
si può utilizzare per nessun tipo di identificatore (variabili, metodi, moduli, classi,
interfacce etc.), invece un nome di tipo riservato ha meno limitazioni. In particolare,
per non impattare troppo sul codice pre-Java 10, è stato deciso che è lecito utilizzare
l'identificatore var
per variabili (sia d'istanza che locali). È persino
possibile dichiarare una variabile locale in questo modo:
var var = 0;
dove il primo var
è il nome di tipo riservato, mentre il secondo è
l'identificatore della variabile.
È anche possibile usare l'identificatore var
per dichiarare un metodo.
Infatti il metodo:
public void var() { }
è sintatticamente corretto.
La parola var
può essere utilizzata anche come identificatore di un package
senza problemi:
package var; //codice omesso
ed anche come identificatore per moduli.
L'unica limitazione che è stata imposta, è che non è possibile dichiarare un tipo Java (ovvero una classe, un'interfaccia,
un'enumerazione, un'annotazione o un record) con l'identificatore var
. Per
esempio la seguente dichiarazione:
class var {}
produrrebbe il seguente errore in compilazione:
error: 'var' not allowed here class var {} ^ as of release 10, 'var' is a restricted local variable type and cannot be used for type declarations 1 error
Notare però che per la convenzione che richiede che i nomi dei tipi inizino con lettera
maiuscola, la possibilità che l'introduzione della parola var
possa causare
danni al codice scritto prima dell'avvento di Java 10 sono davvero minime.
Applicabilità: tipi
È possibile utilizzare la parola var
con tutti i tipi primitivi e
complessi. Per esempio, il seguente snippet compila senza problemi:
var bool = false; // dedotto il tipo boolean var string = "Foqus";// dedotto il tipo String var character= 'J'; // dedotto il tipo char var integer = 8; // dedotto il tipo int var byteInteger = (byte)8; // dedotto il tipo byte var shortInteger = (short)8; // dedotto il tipo short var longInteger = 8L; // dedotto il tipo long var floatingPoint = 3.14F; // dedotto il tipo float var doublePrecisionfloatingPoint = 3.14; // dedotto il tipo double
Come già detto, la deduzione del tipo funziona solo per variabili locali. Quindi
non è applicabile per variabili
d'istanza, per tipi di ritorno di un metodo, per tipo di parametro di un metodo etc.
Inoltre non è possibile utilizzare la parola var
per variabili locali che
non siano inizializzate contestualmente alla dichiarazione, come nel seguente esempio:
var notInitialized;
che produrrà l'output:
error: cannot infer type for local variable notInitialized var notInitialized; ^ (cannot use 'var' on variable without initializer) 1 error
La parola var
, non è neanche utilizzabile nel caso la variabile sia
inizializzata a null
. Infatti:
var nullInitialized = null;
stamperà:
error: cannot infer type for local variable nullInitialized var nullInitialized = null; ^ (variable initializer is 'null') 1 error
La deduzione non può essere utilizzata neanche in caso di dichiarazione multipla di variabili. Per esempio:
var var1 = 1, var2 = 2;
produrrà il seguente errore in compilazione:
error: 'var' is not allowed in a compound declaration var var1 = 1, var2 = 2; ^ 1 error
Infine, non possiamo usare var
per dichiarare array. Per esempio, lo
snippet:
var varArray[] = new int[3];
causerà il seguente errore:
error: 'var' is not allowed as an element type of an array var varArray[] = new int[3]; ^ 1 error
infatti per quanto riguarda la deduzione del tipo per un array, il compilatore già si
basa sulla parte sinistra della dichiarazione
(in inglese left hand side: LHS), mentre la parola var
viene
utilizzata per sfruttare la parte destra (RHS) della dichiarazione.
Applicabilità: cicli
È possibile anche utilizzare la parola var
come tipo dell'indice
nell'inizializzazione di un ciclo for
. Per esempio possiamo scrivere:
String [] strings = {"Antonio", "Ludwig", "Johann Sebastian", "Piotr"}; for (var i = 0; i < strings.length; i++) { System.out.println(strings[i]); }
Infatti, il tipo verrà dedotto dal tipo del valore della parte destra della
assegnazione, nel nostro caso 0
è considerato un int
.
Oppure possiamo usare la parola var
, in luogo del tipo di dato della
variabile temporanea in un ciclo for
migliorato (più noto come ciclo foreach). Quindi il seguente codice è
valido:
int [] arr = {1,2,3,4,5,6,7,8,9}; for (var tmp : arr) { System.out.println(tmp); }
Notiamo che nel precedente esempio, l'utilizzo della parola var
favorisce
l'evoluzione del codice. Infatti, senza modificare il ciclo, possiamo cambiare l'array
a nostro piacimento. Per esempio, le seguenti sono tutte dichiarazioni valide per arr
:
double [] arr = {4.8, 44.5, 100.1, 1.2, 3.0}; boolean [] arr = {true, true, false, true, true, false}; LibroSuJava [] arr = {new LibroSuJava("Il nuovo Java"), new LibroSuJava("Java for Aliens"), new LibroSuJava("Manuale di Java 9")}; JCheckBoxMenuItem []arr = {new JCheckBoxMenuItem("File"), new JCheckBoxMenuItem("Edit"), new JCheckBoxMenuItem("Help")};
Il ciclo foreach funzionerà con qualsiasi di queste dichiarazioni senza dover essere modificato.
Applicabilità: espressioni switch
Possiamo utilizzare la parola var
anche come variabile a cui viene
assegnata un'espressione switch
,
argomento introdotto in anteprima nella versione di Java 12 e definitivamente introdotto con la versione 14. Con l'espressione switch
possiamo
utilizzare il vecchio costrutto switch
come una
poli-espressione, nel senso che può definire
più espressioni (dedicheremo presto un articolo della serie “Superare Java 8” anche a
questo costrutto).
Quando il tipo che deve essere restituito dall'espressione switch
è noto,
allora tutti i case
devono ritornare valori coerenti con il tipo. Questo
significa che il seguente snippet è valido:
String integer = "2"; var index = switch(integer) { case "1"-> { byte b = 1; yield b; } case "2"-> { short s = 2; yield s; } case "3"-> 3; default -> -1; };
Il compilatore infatti, controlla la parte destra (RHS) dell'espressione, anzi, delle
espressioni. Siccome nelle tre espressioni sono ritornati, un byte
, uno
short
e un int
, ovviamente viene dedotto int
come
tipo di ritorno dell'espressione switch
. Questo è singolare visto che se
avessimo utilizzato al posto della parola var
direttamente
int
, il compilatore si sarebbe basato sulla parte sinistra della
dichiarazione (LHS), per stabilire se i tipi che ritornano tutti i case sono
compatibili.
Applicabilità: espressioni lambda e reference a metodi
Come per gli array, anche per le espressioni lambda il compilatore è progettato per
leggere il tipo della variabile nella parte sinistra (LHS) della dichiarazione, mentre
la parola var
, è progettata per sostituire un tipo di dato che si può
dedurre analizzando la parte destra (RHS) della dichiarazione. Questo significa che non
è possibile utilizzare la parola var
con un'espressione lambda locale.
Per esempio, consideriamo la seguente dichiarazione:
Runnable r = () -> System.out.println("Java 8: Funzione anonima");
se proviamo ad utilizzare var
al posto del tipo:
var r = () -> System.out.println("Java 8: Funzione anonima");
otterremmo il seguente errore in compilazione:
error: cannot infer type for local variable r var r = ()->System.out.println("Java 8: Funzione anonima"); ^ (lambda expression needs an explicit target-type) 1 error
Come è prevedibile, neanche con i reference a metodi è possibile utilizzare la parola
var
. Infatti, anche per i reference a metodi la parte sinistra (LHS) della
dichiarazione è decisiva per permettere al compilatore di dedurre i dettagli della
dichiarazione. Con l'utilizzo di var
, otterremmo come al solito un errore
in compilazione. Se per esempio consideriamo la seguente interfaccia funzionale:
@FunctionalInterface public interface Print { void print(Object object); }
possiamo utilizzarla per assegnargli come implementazione il reference del metodo print
dell'oggetto System.out
(print
è un metodo equivalente a
println
, che però dopo la stampa non va a capo):
Print print = System.out::print;
ma se provassimo ad utilizzare var
in luogo del tipo Print
:
var print = System.out::print;
otterremo il seguente errore in compilazione:
error: cannot infer type for local variable print var print = System.out::print; ^ (method reference needs an explicit target-type) 1 error
Applicabilità: classi anonime locali
Sappiamo che una classe anonima viene sempre dichiarata con lo scopo di fare override di
uno o più metodi del tipo che estende. Ora consideriamo la seguente classe che dichiara
una classe anonima che estende la classe
Object
, ma invece di fare override di uno dei suoi metodi, ne definisce uno
nuovo:
public class VarAnonymousTest { public static void main(String args[]) { Object testObject = new Object() { String name ="This can be used with var!"; void test(String test){ System.out.println(test); } }; testObject.test("TEST!");//errore in compilazione } }
Nell'ultima riga proviamo a chiamare il metodo test
utilizzando il
reference testObject
che è di tipo Object
, ottenendo un errore
in compilazione:
VarAnonymousTest.java:9: error: cannot find symbol testObject.test("TEST!");//errore in compilazione ^ symbol: method test(String) location: variable testObject of type Object 1 error
Infatti, una reference di tipo Object
, non può invocare un metodo definito
in un'altra classe. Per poter invocare tale metodo, ci servirebbe un reference del tipo
della classe anonima. Ma un reference della classe anonima, non può esistere, proprio
perché le classi anonime non hanno un nome. Con l'introduzione della parola
var
però, questo limite può essere superato!
Riscriviamo la classe precedente utilizzando la parola var
:
public class VarAnonymousTest { public static void main(String args[]) { var testObject = new Object() { String name ="This can be used with var!"; void test(String test){ System.out.println(test); } }; testObject.test(testObject.name); //funziona! } }
possiamo notare, che abbiamo potuto utilizzare sia la variabile name
, che
il metodo test
, che abbiamo definito ex-novo nella classe anonima locale.
In questo caso quindi, l'introduzione della parola var
, oltre a
rappresentare uno strumento per ridurre la verbosità di Java, ci ha permesso di superare
un limite del linguaggio.
Conclusioni
In questo articolo abbiamo introdotto la parola var
, ed abbiamo visto come
possiamo utilizzarla a posto del tipo delle variabili locali. Il vantaggio evidente
dell'utilizzo di var
, è quello di snellire il codice, ma non è l'unico.
Per esempio, abbiamo visto che supporta l'evoluzione del software, e che
sorprendentemente, rende le classi anonime locali più utili e potenti, permettendoci di
sfruttare nuove soluzioni che in passato non potevamo sfruttare. La deduzione del tipo
per le variabili locali rappresenta quindi un altro dei motivi per superare Java 8.