1. Approfondiamo le classi record
Prima di tuffarci nei record patterns, rivediamo che cos'è una classe record.
Record — è una classe compatta e immutabile con metodi equals, hashCode, toString generati automaticamente, oltre alla creazione automatica dei getter per tutti i componenti. Le classi record sono arrivate in Java 16 e sono state una vera manna per chi era stanco di scrivere manualmente i DTO.
// Classico: Point con due campi
record Point(int x, int y) {}
Creazione dell'oggetto e accesso ai campi:
Point p = new Point(10, 20);
System.out.println(p.x()); // 10
System.out.println(p.y()); // 20
System.out.println(p); // Point[x=10, y=20]
Perché servono i record patterns
Fino a Java 21, se volevi verificare che un oggetto fosse un record e ottenerne i campi, bisognava scrivere molto codice manuale: verifica del tipo con instanceof, cast e chiamata dei getter.
Object obj = new Point(5, 7);
if (obj instanceof Point) {
Point p = (Point) obj;
int x = p.x();
int y = p.y();
System.out.println("x=" + x + ", y=" + y);
}
Se il record è complesso o annidato, il codice si trasforma rapidamente in una “giungla di estrazioni”. E usarlo dentro uno switch era scomodo.
I record patterns permettono di decomporre i valori delle classi record direttamente nel pattern matching: sia in instanceof sia in switch. Questo rende il codice più corto, più sicuro e più chiaro.
2. Sintassi dei record patterns: l'esempio più semplice
record Point(int x, int y) {}
Object obj = new Point(10, 20);
if (obj instanceof Point(int x, int y)) {
System.out.println("x=" + x + ", y=" + y);
}
Che cosa succede qui?
- obj instanceof Point(int x, int y) — verifichiamo il tipo e decomponiamo subito i componenti nelle variabili x e y.
- Le variabili x e y sono disponibili solo all'interno del blocco if.
Risultato:
x=10, y=20
Attenzione: se obj non è un Point oppure è null, il blocco if non verrà eseguito e le variabili non saranno dichiarate.
3. Record patterns in switch: concisione e sicurezza
record Point(int x, int y) {}
record Circle(Point center, int radius) {}
Object shape = new Point(3, 4);
switch (shape) {
case Point(int x, int y) -> System.out.println("Point: x=" + x + ", y=" + y);
case Circle(Point center, int r) -> System.out.println("Circle with radius " + r);
default -> System.out.println("Unknown shape!");
}
- Nel case Point(int x, int y) otteniamo subito accesso a x e y.
- Nel case Circle(Point center, int r) sono disponibili center e r.
Risultato:
Point: x=3, y=4
Con gerarchie sealed lo switch diventa esaustivo: se dimentichi di gestire un caso, il compilatore ti avvertirà.
4. Pattern annidati per record: decomposizione annidata
record Point(int x, int y) {}
record Line(Point start, Point end) {}
Object obj = new Line(new Point(1, 2), new Point(3, 4));
if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
System.out.println("Line from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")");
}
Risultato:
Line from (1,2) to (3,4)
Lavori con alberi, grafi e strutture complesse? I pattern annidati sono i tuoi alleati.
5. Record patterns con guard (when)
Object obj = new Point(0, 100);
if (obj instanceof Point(int x, int y) && x == 0) {
System.out.println("Il punto è sull'asse Y: y=" + y);
}
In uno switch si fa con when:
switch (obj) {
case Point(int x, int y) when x == 0 -> System.out.println("Sull'asse Y: y=" + y);
case Point(int x, int y) when y == 0 -> System.out.println("Sull'asse X: x=" + x);
case Point(int x, int y) -> System.out.println("Punto normale");
default -> System.out.println("Non è un punto");
}
6. Applicazione dei record patterns in un'applicazione reale
Prendiamo un esempio pratico — figure geometriche.
record Point(int x, int y) {}
record Circle(Point center, int radius) {}
record Rectangle(Point topLeft, int width, int height) {}
public static void printShapeInfo(Object shape) {
switch (shape) {
case Point(int x, int y) -> System.out.println("Point: (" + x + ", " + y + ")");
case Circle(Point(int x, int y), int radius) ->
System.out.println("Circle: center=(" + x + ", " + y + "), radius=" + radius);
case Rectangle(Point(int x, int y), int width, int height) ->
System.out.println("Rectangle: topLeft=(" + x + ", " + y + "), size=" + width + "x" + height);
default -> System.out.println("Unknown shape");
}
}
Esempio di chiamata:
printShapeInfo(new Rectangle(new Point(5, 10), 20, 30));
Risultato:
Rectangle: topLeft=(5, 10), size=20x30
7. Limitazioni e particolarità d'uso dei record patterns
Solo per classi record. I record patterns funzionano solo con oggetti che sono effettivamente classi record.
class NotARecord {
int a, b;
NotARecord(int a, int b) { this.a = a; this.b = b; }
}
// Errore! Non è un record
// if (obj instanceof NotARecord(int a, int b)) { ... }
Corrispondenza per numero e tipo dei componenti. Il pattern deve corrispondere ai componenti della classe record.
record Pair(int a, String b) {}
// Errore: i tipi non corrispondono
// if (obj instanceof Pair(String a, int b)) { ... }
Le variabili sono disponibili solo all'interno del blocco. I nomi dichiarati nel pattern sono visibili nel corpo dello specifico if o case in cui il matching ha successo.
8. Pattern annidati: esempio con un albero
sealed interface Expr permits NumberExpr, PlusExpr, MinusExpr {}
record NumberExpr(int value) implements Expr {}
record PlusExpr(Expr left, Expr right) implements Expr {}
record MinusExpr(Expr left, Expr right) implements Expr {}
int eval(Expr expr) {
return switch (expr) {
case NumberExpr(int value) -> value;
case PlusExpr(Expr left, Expr right) -> eval(left) + eval(right);
case MinusExpr(Expr left, Expr right) -> eval(left) - eval(right);
};
}
Esempio d'uso:
Expr e = new PlusExpr(new NumberExpr(7), new MinusExpr(new NumberExpr(10), new NumberExpr(3)));
System.out.println(eval(e)); // 7 + (10 - 3) = 14
Qui decomponiamo direttamente NumberExpr(int value), PlusExpr(Expr left, Expr right) e MinusExpr(Expr left, Expr right) — il codice diventa conciso ed espressivo.
9. Tabella: confronto tra pattern matching con e senza record patterns
| Approccio | Cast manuale del tipo | Pattern matching con record patterns |
|---|---|---|
| Verifica del tipo | |
|
| Cast del tipo | |
Non necessario |
| Accesso ai campi | |
|
| Decomposizione annidata | Molto codice manuale | Pattern annidati |
| Uso in switch | Scomodo/impossibile | Facile e conciso |
10. Errori tipici nell'uso dei record patterns
Errore n. 1: tentare di applicare un record pattern a una classe non record. Se scrivi una classe normale e poi provi a “decomporla” come un record, il compilatore non lo permetterà. I record patterns funzionano solo con vere classi record.
Errore n. 2: numero o tipo di componenti non corrispondente. Il pattern deve corrispondere esattamente alla firma dei componenti del record. Per esempio, per record Pair(int a, String b) non puoi scrivere Pair(int x, int y).
Errore n. 3: provare a usare le variabili del pattern fuori dal blocco. I nomi dichiarati nel pattern sono disponibili solo all'interno del relativo if/case. Fuori dal blocco tali variabili non esistono.
Errore n. 4: usare i record patterns su vecchie versioni di JDK. Il supporto è arrivato in Java 21. Su Java 17 e precedenti otterrai un errore di sintassi. Verifica la versione di JDK e il supporto nell'IDE.
Errore n. 5: pattern annidati troppo profondi. Le possibilità di annidamento sono potenti, ma non complicare senza necessità: una struttura troppo profonda peggiora la leggibilità e aumenta il rischio di errori.
GO TO FULL VERSION