Superare Java 8: var

Superare Java 8: deduzione del tipo per le variabili locali (var)

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.

Note dell'autore

Anche ignorando la maggiore sicurezza che offrono le ultime versioni del JDK, esistono tantissimi motivi per aggiornare le proprie conoscenze di Java, o quantomeno le proprie installazioni del runtime di Java. Il mio ultimo libro “Il nuovo Java”, a cui si ispira la serie “Superare Java 8”, contiene tutte le informazioni per poter imparare Java da zero, ed utilizza un metodo didattico ben collaudato e perfezionato in 20 anni di esperienza, che rende l'apprendimento semplice e appassionante. Inoltre è strutturato per approfondire gli argomenti ed avere una conoscenza superiore che può fare la differenza per la vostra carriera lavorativa.
Questo articolo è ispirato a vari paragrafi dei capitoli 3 e 13 del libro “Il nuovo Java”. In realtà, l'argomento è approfondito anche in altri capitoli dove si valuta l'impatto di questa caratteristica per esempio sul polimorfismo.
Per maggiori informazioni visitate https://www.nuovojava.it.

Lascia un commento

Una email sarà mandata a
claudio@claudiodesio.com


Caricamento
Il tuo messaggio è stato mandato. Grazie!