Al momento stai visualizzando Superare Java 8: pattern matching per instanceof

Superare Java 8: pattern matching per instanceof

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 è un articolo che fa parte di una serie 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 vedremo una interessante novità introdotta nella versione 14 come caratteristica in anteprima (feature preview, vedi relativo articolo), ed ufficializzata definitivamente come caratteristica standard con Java 16. Si tratta della prima parte di una articolata caratteristica nota come pattern matching. Il pattern matching toccherà vari costrutti di programmazione in futuro, per ora è disponibile solo il pattern matching che riguarda l’operatore instanceof. Questo cambierà radicalmente il modo in cui utilizziamo tale operatore.

 

L’operatore instanceof

L’operatore binario instanceof, come gli operatori di confronto, restituisce un valore booleano. La particolarità di questo operatore, è che utilizza come primo operando un reference, e come secondo operando un tipo complesso. Restituisce true, se al runtime, il reference (che definisce il primo operando) punta ad un oggetto istanziato dal tipo (che definisce il secondo operando). Restituisce true, anche nel caso il reference punti ad un oggetto istanziato da una sottoclasse del tipo specificato con il secondo operando. Se non si verifica una di queste due condizioni, l’operatore instanceof, restituisce false.

 

Esempio

Supponiamo di voler creare un sistema che stabilisca le paghe dei dipendenti di un’azienda, considerando le seguenti classi:

public class Dipendente {
    private String nome;
    private int stipendio;
    private int matricola;
    private String dataDiNascita;
    private String dataDiAssunzione;
    //metodi setter e getter omessi
}

public class Programmatore extends Dipendente {
    private String linguaggiConosciuti;
    private int anniDiEsperienza;
    //metodi setter e getter omessi
}

public class Dirigente extends Dipendente {
    private String orarioDiLavoro;
    //metodi setter e getter omessi
}

public class AgenteDiVendita extends Dipendente {
    private String [] portafoglioClienti;
    private int provvigioni;
    //metodi setter e getter omessi
}

Supponiamo di dover pagare la paghe a tutte le tipologie di dipendenti, potremmo iniziare a raggruppare tutti i dipendenti dell’azienda, all’interno di una collezione eterogenea:

Dipendente [] arr = new Dipendente [180];
arr[0] = new Dirigente();
arr[1] = new Programmatore();
arr[2] = new AgenteDiVendita();
//... altre assegnazioni omesse

Per gestire la paga dei dipendenti, potremmo creare il seguente metodo.

public void pagaDipendente(Dipendente dip) {
    if (dip instanceof Programmatore) {
        dip.setStipendio(1500);
    }
    else if (dip instanceof Dirigente) {
        dip.setStipendio(3000);
    }
    else if (dip instanceof AgenteDiVendita) {
        dip.setStipendio(1000);
    }
    //...
}

Esso sfrutta l’operatore instanceof per testare a che tipo punta il parametro polimorfo dip, e settare lo stipendio di conseguenza.

Potremmo invocare questo metodo all’interno di un ciclo foreach (di 180 iterazioni), passandogli tutti gli elementi della collezione eterogenea, e raggiungere così il nostro scopo:

//...
for (Dipendente dipendente : arr) {
    pagaDipendente(dipendente);
    //...
}

 

Precisazione

Siccome instanceof restituisce true anche nel caso il reference punti ad un oggetto istanziato da una sottoclasse del tipo specificato con il secondo operando, bisogna tener presente che, scrivendo il metodo pagaDipendente nel seguente modo:

public void pagaDipendente(Dipendente dip) {
    if (dip instanceof Dipendente) {
        dip.setStipendio(1000);
    }
    else if (dip instanceof Programmatore) {
    //...

tutti i dipendenti sarebbero pagati allo stesso modo. Infatti, anche se dip fosse un reference di tipo Programmatore, sarebbe comunque verificato il controllo del primo if. Questa situazione è coerente con la relazione is a. Infatti, un oggetto Programmatore è anche un Dipendente.

 

Cast di oggetti

Nell’esempio precedente, abbiamo osservato che l’operatore instanceof ci permette di testare a quale tipo di istanza punta un reference. Abbiamo già visto però, che un reference che punta ad un oggetto istanziato da una sottoclasse, non può accedere ai membri dichiarati nella sottoclasse. Per esempio, supponiamo che lo stipendio di un programmatore dipenda dal numero di anni di esperienza. In questa situazione, dopo aver testato che il reference dip punta ad un’istanza di Programmatore, avremo bisogno di accedere alla variabile anniDiEsperienza. Se tentassimo di accedervi mediante la sintassi:

dip.getAnniDiEsperienza();

otterremmo un errore in compilazione. Infatti, il reference dip, essendo di tipo Dipendente, non potrà accedere ai metodi che sono dichiarati nelle sottoclassi. Per superare questo ostacolo, possiamo utilizzare il meccanismo del cast di oggetti. In pratica, dobbiamo dichiarare un reference di tipo Programmatore e farlo puntare all’indirizzo di memoria dove punta il reference dip, utilizzando il cast per confermare l’intervallo di puntamento. Il nuovo reference, essendo di tipo Programmatore, ci permetterà di accedere a qualsiasi membro dell’istanza di Programmatore.

Il cast di oggetti sfrutta una sintassi del tutto simile al cast tra dati primitivi:

if (dip instanceof Programmatore) {
    Programmatore pro = (Programmatore) dip;
    if (pro.getAnniDiEsperienza() > 2)
//...

Si noti che ora siamo in grado di accedere alla variabile incapsulata anniDiEsperienza.

Nella figura 1 schematizziamo idealmente la situazione con un grafico, dove l’oggetto viene puntato dai puntatori dip di tipo Dipendente e pro di tipo Programmatore.

Figura 1 - Due diversi tipi di accesso per lo stesso oggetto.

Nella figura 1 i due reference puntano allo stesso oggetto, ma con un differente intervallo di puntamento. È fondamentale utilizzare il cast di oggetti solo dopo averne controllato la validità con l’operatore instanceof. Infatti, il compilatore non può stabilire se ad un certo indirizzo risiede un determinato oggetto invece che un altro. È solo in fase di esecuzione che la Java Virtual Machine può sfruttare l’operatore instanceof per risolvere ogni dubbio.

 

Qual è il problema?

L’utilizzo combinato dell’operatore instanceof e del cast di oggetti, non si può definire una best practice di Java, ma sicuramente è un pattern di programmazione ben conosciuto e molto utilizzato. Quello che però risulta evidente, è che tale pratica è indubbiamente verbosa. Inoltre è ripetitiva, nell’esempio precedente abbiamo nominato il tipo Programmatore tre volte in due righe. Infine, dopo un test con instanceof, è scontato che la prossima istruzione sia un cast. Infatti, alcuni IDE permettono di automatizzare questa pratica. Se però non automatizziamo la codifica con un IDE, non è improbabile affidarsi ad un copia-incolla se questo codice si ripete più volte, e potremmo andare anche incontro ad una ClassCastException al runtime.

 

Pattern matching per instanceof

Java 16 ha introdotto la prima parte di una articolata caratteristica chiamata pattern matching. In questa caratteristica, un pattern (da non confondersi con un design pattern) è composto da:

  • un predicato: un test che cerca la corrispondenza (matching) di un input con un suo operando;
  • una o più variabili di pattern (sino a Java 15 note come variabili di binding),: queste vengono estratte dall’operando a seconda del risultato del test.

Un pattern è quindi un modo sintetico di esprimere una soluzione complessa.

Quando questa caratteristica sarà implementata completamente, toccherà anche altri argomenti come i tipi record, ed il costrutto switch. Nella versione 15, il pattern matching è ancora presentato come feature preview, e l’unica parte implementata riguarda proprio il pattern che coinvolge l’operatore instanceof e il cast di oggetti che abbiamo appena visto. In pratica, abilitando le feature preview, possiamo usare una sintassi alternativa:

if (dip instanceof Programmatore pro) {
    if (pro.getAnniDiEsperienza() > 2)
    //...
}
//...

Tale sintassi, specifica un reference pro di tipo Programmatore, accanto all’operazione di instanceof. Questo reference può essere subito utilizzato, e non è necessario alcun cast, visto che è già di tipo Programmatore. La sintassi quindi, è molto semplice, bisogna però fare qualche osservazione.

 

Visibilità di pattern

In particolare, il reference pro, viene definito come variabile di pattern, ovvero una particolare variabile locale, con un nuovo tipo di visibilità che dipende dal pattern. Come una variabile locale può convivere con variabili d’istanza con lo stesso identificatore. La differenza consiste nel fatto che la sua visibilità dipende dal risultato del predicato, ovvero se è vero o falso il risultato del test dell’instanceof. Nell’esempio precedente, il reference pro, era visibile solo nel blocco di codice relativo al costrutto if. Se aggiungessimo la clausola else, e anteponessimo l’operatore NOT al test dell’instanceof nel seguente modo:

if (!dip instanceof Programmatore pro) {
    //il reference pro NON può essere utilizzato qui
} else {
    //il reference pro può essere utilizzato qui!
}

allora il reference pro, sarà visibile solo nel blocco di codice della clausola else, e non nel blocco di codice dell’if.

 

Costanti di pattern?

Sino alla versione 15, una variabile di pattern era implicitamente final, e non era possibile modificare il suo indirizzamento. Per esempio:

if (dip instanceof Programmatore pro) {
    pro = new Programmatore();
    //...
}

avrebbe causato il seguente errore di compilazione in Java 15:

error: pattern binding pro may not be assigned
            pro = new Programmatore();
            ^
1 error

Questo era il comportamento delle variabili di pattern in Java 15, dove il pattern matching per instanceof era ancora una caratteristica in anteprima. Ma nella versione 16 il pattern matching per instanceof è stato ufficializzato come caratteristica standard di Java e questa limitazione è stata eliminata definitivamente.

In qualsiasi caso, tramite una variabile di pattern, essendo un reference, è comunque possibile modificare lo stato interno dell’oggetto a cui punta. Per esempio, il seguente codice è valido:

if (dip instanceof Programmatore pro) {
    pro.setAnniDiEsperienza(8);
}

infatti non viene assegnato un nuovo indirizzo al reference pro, ma solo invocato un metodo su di esso.

 

Conclusioni

In questo articolo abbiamo visto come Java 14 abbia introdotto come feature preview il pattern matching per instanceof. Questa nuova caratteristica introduce un nuovo tipo di visibilità per le variabili di pattern, e modificherà per sempre il nostro modo di usare l’operatore instanceof e il cast di oggetti.

Il pattern matching per instanceof, rappresenta quindi un altro piccolo passo in avanti per il linguaggio, ma anche un blocco fondamentale per il pattern matching per switch, introdotto come feature preview nella versione 17.

 

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 principalmente al paragrafo 7.3 del capitolo 7 del libro “Il nuovo Java”.

Per maggiori informazioni visitate https://www.nuovojava.it.

Lascia un commento