Pattern Matching con instanceof in Java

Concetti Chiave
  • Il pattern matching con instanceof consente di verificare il tipo di un oggetto e di assegnare una variabile pattern in un'unica espressione.
  • Questa funzionalità semplifica il codice, riducendo la necessità di cast espliciti e dichiarazioni separate.
  • Il pattern matching può essere utilizzato in istruzioni if, cicli for e while, ma non è consigliato in cicli do a causa della limitazione della variabile pattern.
  • Il pattern matching è stato introdotto in JDK 16 e rappresenta un miglioramento significativo rispetto alla forma tradizionale di instanceof.

Pattern Matching con instanceof

Nella lezione precedente abbiamo studiato la forma tradizionale dell'operatore instanceof.

Come spiegato lì, instanceof restituisce true se un oggetto è di un tipo specificato, o può essere convertito a quel tipo. A partire da JDK 16, è stata aggiunta a Java una seconda forma dell'operatore instanceof che supporta la nuova funzionalità di pattern matching.

In termini generali, il pattern matching definisce un meccanismo che determina se un valore si adatta a una forma generale. Per quanto riguarda instanceof, il pattern matching viene utilizzato per testare il tipo di un valore (che deve essere un tipo di riferimento) contro un tipo specificato. Questo tipo di pattern è chiamato type pattern. Se il pattern corrisponde, una pattern variable riceverà un riferimento all'oggetto che corrisponde al pattern.

La forma pattern matching di instanceof è mostrata qui:

riferimento instanceof tipo pattern_var;

Se instanceof ha successo, pattern_var verrà creata e conterrà un riferimento all'oggetto che corrisponde al pattern. Se fallisce, pattern_var non viene mai creata. Questa forma di instanceof ha successo se l'oggetto riferito da riferimento può essere convertito in tipo e il tipo statico di riferimento non è un sottotipo di tipo.

Ad esempio, il seguente frammento crea un riferimento Number chiamato mioOb che si riferisce a un oggetto Integer. Ricordiamo che Number è una super-classe di tutti i wrapper di tipo primitivo numerico.

Quindi utilizza l'operatore instanceof per confermare che l'oggetto riferito da mioOb è un Integer. Questo risulta in un oggetto chiamato iOgg di tipo Integer che viene istanziato e che contiene il valore corrispondente.

Number mioOb = Integer.valueOf(9);

// Usa la versione pattern matching di instanceof.
if(mioOb instanceof Integer iOgg) {
    // iOgg è un oggetto valido di tipo Integer.
    // all'interno dell'ambito dell'if.
    System.out.println("iOgg si riferisce a un intero: " + iOgg); 
}

// iOgg non esiste più qui

Come indicano i commenti, iOgg è valido solo all'interno dello scope della clausola if. Non è valido al di fuori dell'if. Non sarebbe valido nemmeno all'interno di una clausola else, se ne fosse stata inclusa una. È cruciale capire che la pattern variable iOgg viene creata solo se il pattern matching ha successo.

Il vantaggio principale della forma pattern matching di instanceof è che riduce la quantità di codice che era tipicamente necessaria con la forma tradizionale di instanceof. Ad esempio, consideriamo questa versione funzionalmente equivalente dell'esempio precedente che utilizza l'approccio tradizionale:

// Usa un instanceof tradizionale.
if(mioOb instanceof Integer) {
    // Usa un cast esplicito per ottenere iOgg.
    Integer iOgg = (Integer) mioOb;
    System.out.println("iOgg si riferisce a un intero: " + iOgg);
}

Con la forma tradizionale, sono richiesti una dichiarazione separata e un cast esplicito per creare la variabile iOgg. La forma pattern matching di instanceof semplifica il processo.

Variabili Pattern in un'Espressione AND Logica

Un instanceof può essere utilizzato in un'espressione AND logica. Tuttavia, è necessario ricordare che la variabile pattern è in scope solo dopo che è stata creata. Ad esempio, il seguente if ha successo solo quando mioOgg si riferisce a un Integer e il suo valore è non negativo.

Si presti particolare attenzione all'espressione nell'if:

if((mioOgg instanceof Integer oggInt) && (oggInt >= 0)) {
    // mioOgg è sia un Integer che non negativo.
    // ...
}

La variabile pattern oggInt viene creata solo se il lato sinistro dell'&& (la parte che contiene l'operatore instanceof) è true. Si noti che oggInt è utilizzata anche dal lato destro. Questo è possibile perché viene utilizzata la forma a corto circuito dell'operatore AND logico, e il lato destro viene valutato solo se il sinistro ha successo. Quindi, se l'operando del lato destro viene valutato, oggInt sarà in scope. Tuttavia, se avessimo provato a scrivere l'istruzione precedente utilizzando l'operatore & così:

// ERRORE: Usa l'operatore & invece di &&
if((mioOgg instanceof Integer oggInt) & (oggInt >= 0)) {
    // mioOgg è sia un Integer che non negativo.
    // ...
}

si verificherebbe un errore di compilazione perché oggInt non sarà in scope se il lato sinistro fallisce. Ricorda che l'operatore & causa la valutazione di entrambi i lati dell'espressione, ma oggInt è in scope solo se il lato sinistro è true. Questo errore viene catturato dal compilatore. Una situazione correlata si verifica con questo frammento:

int conteggio = 10;

// Corretto
if((conteggio < 100) && mioOgg instanceof Integer oggInt) {
    // mioOgg è sia un Integer che non negativo, e conteggio è minore di 100.
    oggInt = conteggio;
    // ...
}

Questo frammento compila perché il blocco if si eseguirà solo quando entrambi i lati dell'&& sono true. Quindi, l'uso di oggInt nel blocco if è valido. Tuttavia, si verificherebbe un errore di compilazione se avessimo provato a usare & invece di &&, come mostrato qui:

// ERRORE: Usa l'operatore & invece di &&
if((conteggio < 100) & mioOgg instanceof Integer oggInt) {
    /* ... */
}

In questo caso, il compilatore non può sapere se oggInt sarà in scope nel blocco if perché il lato destro dell'& non sarà necessariamente valutato.

Un altro punto: Un'espressione logica non può introdurre la stessa variabile pattern più di una volta. Ad esempio, in un AND logico, è un errore se entrambi gli operandi creano la stessa variabile pattern.

Pattern Matching in Altre Istruzioni

Sebbene un uso frequente della forma di pattern matching di instanceof sia in un'istruzione if, non è affatto limitato a tale uso. Può anche essere impiegato nella parte condizionale delle istruzioni di ciclo. Come esempio, immaginiamo che stiamo elaborando una collezione di oggetti, forse contenuti in un array. Inoltre, all'inizio dell'array ci sono diverse stringhe, e vogliamo elaborare quelle stringhe, ma non gli altri oggetti rimanenti nella lista. La seguente sequenza porta a termine questo compito con un ciclo for in cui la condizione usa instanceof per confermare che un oggetto nell'array sia una String e per ottenere quella stringa per l'elaborazione all'interno del ciclo. Quindi, il pattern matching viene utilizzato per controllare l'esecuzione di un ciclo for e per ottenere il prossimo valore per l'elaborazione.

Object[] alcuniOggetti = {
    new String("Alfa"),
    new String("Beta"),
    new String("Omega"),
    Integer.valueOf(10)
};

int i;

// Questo ciclo itera finché un elemento non è una String,
// o la fine dell'array viene raggiunta.
for(i = 0; (alcuniOggetti[i] instanceof String str) && (i < alcuniOggetti.length); i++) {
    System.out.println("Elaborando " + str);
    // ...
}

System.out.println("Le prime " + i + " voci nella lista sono stringhe.");

L'output di questo frammento è mostrato qui:

Elaborando Alfa
Elaborando Beta
Elaborando Omega
Le prime 3 voci nella lista sono stringhe.

La forma di pattern matching di instanceof può anche essere utile in un ciclo while. Ad esempio, qui c'è il precedente ciclo for, ricodificato come un while:

i = 0;
while((alcuniOggetti[i] instanceof String str) && (i < alcuniOggetti.length)) {
    System.out.println("Elaborando " + str);
    i++;
}

Sebbene sia tecnicamente possibile utilizzare il pattern matching instanceof nella parte condizionale di un ciclo do, tale uso è severamente limitato perché la variabile pattern non può essere utilizzata nel corpo del ciclo perché non sarà nello scope finché l'operatore instanceof non viene eseguito.