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 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 vedremo una interessante novità introdotta nella versione 14 come
caratteristica in anteprima (o feature preview, vedi
relativo articolo). Si tratta della prima parte di una articolata caratteristica
nota come pattern matching (che potremmo
tradurre come corrispondenza di modello). Il
pattern matching toccherà vari costrutti di programmazione in futuro, per ora è
disponibile in anteprima 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); //... }
Cast di oggetti
Nell'esempio precedente, abbiamo osservato che l'operatore instanceof
ci
permette di testare a quale tipo di istanza punta un reference. Sappiamo già però,
che un reference di tipo superclasse 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:
error: cannot find symbol dip.getAnniDiEsperienza(); ^ symbol: method getAnniDiEsperienza() location: variable dip of type Dipendente
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 14 ha introdotto come feature preview, la prima parte di una articolata caratteristica nota come pattern matching (che potremmo tradurre come corrispondenza di modello). 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 binding (che potremmo tradurre come variabili vincolate) : queste vengono estratte dall'operando a seconda del risultato del test.
Un pattern è quindi un modo sintetico di esprimere un
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 binding
In particolare, il reference pro
, viene definito come variabile di binding,
ovvero una particolare variabile locale, con un nuovo tipo di visibilità vincolata 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 binding?
Inoltre, una variabile di binding è implicitamente final
, e non è possibile
modificare il suo indirizzamento. Per esempio:
if (dip instanceof Programmatore pro) { pro = new Programmatore(); //... }
causerebbe il seguente errore di compilazione:
error: pattern binding pro may not be assigned pro = new Programmatore(); ^ 1 error
Questa è attualmente il comportamento delle variabili di binding in Java 15, dove il
pattern matching per instanceof
è ancora una
caratteristica in anteprima. Ma nella versione 16 che
sarà pubblicata nel marzo del 2021, il pattern matching per instanceof
verrà ufficializzato come caratteristica standard di Java, e questa limitazione dovrebbe
essere eliminata definitivamente. In qualsiasi caso, tramite una variabile di binding,
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.
Java 16
Allo stato attuale, l'evoluzione finale del pattern matching per
instanceof
non è ancora definita completamente, e altre modifiche
potrebbero essere apportate. Quando avremo la possibilità di testare la versione
standard di questa caratteristica, torneremo ad aggiornare questo articolo.
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 binding, e modificherà per sempre il nostro modo di usare
l’operatore instanceof
e il cast di oggetti. In Java 15, tale
caratteristica è rimasta ancora in preview, ma nella versione 16 , grazie anche ai
feedback ricevuti dagli utenti, ci saranno delle modifiche.
Il pattern matching per instanceof
, rappresenta quindi un altro piccolo
passo in avanti per il linguaggio.