Pattern Matching in Java
Usa il pattern matching in Java per instanceof e switch — pattern di tipo, record pattern e decostruzione.
Per anni, il codice Java che lavorava con valori di tipo sconosciuto seguiva un rituale noioso: verificare il tipo con instanceof, poi eseguire il cast a quel tipo, poi usarlo. Il pattern matching riduce questo rituale a una singola espressione. Un pattern descrive la forma dei dati; se un valore corrisponde, Java associa le sue parti a variabili utilizzabili immediatamente — senza cast manuali.
Il pattern matching è arrivato per fasi: prima i pattern per instanceof, poi i pattern in switch, poi i record pattern che decostruiscono i record nei loro componenti. Insieme permettono di scrivere codice dichiarativo e type-safe che rispecchia i dati su cui opera.
Questo capitolo tratta il pattern per instanceof, i pattern di tipo in switch, i pattern con guardia e la gestione di null, e i record pattern — poi li collega in un programma eseguibile. Si basa su tre funzionalità che potresti voler rivedere prima: l'operatore instanceof, i record e le espressioni switch.
Pattern Matching per instanceof
Il classico pattern test-e-cast richiedeva tre riferimenti allo stesso tipo. Il pattern per instanceof associa una variabile nello stesso momento del test, e l'associazione è in scope ovunque il test risulti vero.
Object value = "hello";
// Old way: test, then cast
if (value instanceof String) {
String s = (String) value;
System.out.println(s.length());
}
// Pattern way: test and bind together
if (value instanceof String s) {
System.out.println(s.length());
}Poiché la variabile di associazione partecipa all'espressione booleana, puoi continuare a restringere nello stesso if. Il compilatore verifica che s sia sicuro da usare:
if (value instanceof String s && s.length() > 3) {
System.out.println(s.toUpperCase());
}Pattern in switch
Uno switch può eseguire la corrispondenza su pattern di tipo, effettuando il dispatch in base al tipo runtime del selettore. Ogni case associa il valore corrisposto, così il corpo lavora direttamente con una variabile tipizzata. Questo trasforma lunghe catene if/else instanceof in una tabella compatta e leggibile.
static String format(Object value) {
return switch (value) {
case Integer i -> "int: " + i;
case Long l -> "long: " + l;
case String s -> "string: " + s;
default -> "other: " + value;
};
}Uno switch con pattern di tipo deve essere esaustivo — deve coprire ogni possibile input. Per selettori di tipo Object arbitrario ciò significa un ramo default; per le gerarchie sealed il compilatore conosce l'insieme completo dei sottotipi e può verificare l'esaustività senza un default.
Pattern con Guardia e null
Una clausola when aggiunge una condizione booleana a un case, permettendo a due valori dello stesso tipo di prendere rami diversi. Questo si chiama pattern con guardia, e l'ordine conta: i case con guardia più specifici vengono prima del fallback senza guardia.
static String size(String s) {
return switch (s) {
case String t when t.isEmpty() -> "empty";
case String t when t.length() < 5 -> "short";
case String t -> "long (" + t.length() + ")";
};
}Tradizionalmente uno switch lanciava NullPointerException su un selettore null. Uno switch con pattern può gestire null esplicitamente con un case null, mantenendo il controllo null all'interno dello stesso costrutto invece di una guardia separata prima di esso.
| Funzionalità | Sintassi | Scopo |
|---|---|---|
| Pattern di tipo | case String s | Corrispondenza per tipo e associazione |
| Pattern con guardia | case String s when s.isEmpty() | Aggiunge una condizione a un case |
| Etichetta null | case null | Corrisponde a un selettore null |
| Record pattern | case Point(int x, int y) | Decostruisce un record |
Record Pattern
Un record pattern corrisponde a un record e associa i suoi componenti in un'unica operazione, evitando le chiamate agli accessor. Poiché i record espongono i loro componenti, il compilatore conosce la forma esatta e permette di nominare ogni parte inline. I record pattern si annidano, quindi puoi destrutturare un record di record.
record Point(int x, int y) {}
record Line(Point start, Point end) {}
static String render(Object o) {
return switch (o) {
case Point(int x, int y) -> "point " + x + "," + y;
// Nested: pull both endpoints' coordinates out at once
case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
"line " + x1 + "," + y1 + " -> " + x2 + "," + y2;
default -> "unknown";
};
}Il pattern matching eccelle con i tipi sealed: quando un'interfaccia elenca le sue implementazioni consentite, uno switch su di esse è esaustivo senza un default, e l'aggiunta di un nuovo sottotipo trasforma il case mancante in un errore di compilazione invece di un bug silenzioso.
Un Esempio Completo ed Eseguibile
Il programma seguente collega tutti i pezzi insieme. Usa un pattern instanceof con una guardia, una gerarchia Shape sealed di record, record pattern che decostruiscono ogni forma in uno switch, un pattern con guardia che individua un quadrato, e un case null — tutto senza un singolo cast esplicito.
Cosa ricavare dall'esecuzione:
describe(42)stampapositive int 42perché la guardiainstanceof Integer i && i > 0verifica il tipo e il valore insieme prima di associarei.describe(-5)ricade suunknown— lo stesso patternIntegercorrisponde al tipo ma la guardiai > 0fallisce, mostrando come una guardia raffina un pattern di tipo.- Lo
switchdiareanon necessita didefault:Shapeè sealed, quindi elencareCircle,RectangleeTriangleè esaustivo e il compilatore è soddisfatto. - Il rettangolo
5.0 x 5.0viene stampato comesquare side=5.0perché il suo case con guardiawhen w == hè posizionato prima del case generaleRectangle re ha la precedenza. - L'ultima riga stampa
no shape: il ramocase nullgestisce un selettorenullall'interno dello switch invece di lanciareNullPointerException.