1. Zagłębiamy się w klasy record
Zanim zanurzymy się w record patterns, odświeżmy sobie, czym jest klasa record.
Record — to zwięzła, niemutowalna klasy z automatycznie generowanymi metodami equals, hashCode, toString, a także z automatycznym tworzeniem getterów dla wszystkich komponentów. Klasy record pojawiły się w Javie 16 i były prawdziwym wybawieniem dla wszystkich, którzy mieli dość ręcznego pisania „DTO-ki”.
// Klasyka: Point z dwoma polami
record Point(int x, int y) {}
Tworzenie obiektu i dostęp do pól:
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]
Po co nam Record Patterns
Przed Javą 21, jeśli chcieliście sprawdzić, że obiekt — to pewien record, i pobrać jego pola, trzeba było pisać sporo ręcznego kodu: sprawdzenie typu przez instanceof, rzutowanie i wywołanie getterów.
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);
}
Jeśli record jest złożony lub zagnieżdżony, kod szybko zamienia się w „las rozpakowań”. A używanie takiego rozpoznawania w switch było wcześniej niewygodne.
Record patterns pozwalają rozpakowywać wartości klas record bezpośrednio w dopasowaniu wzorców: zarówno w instanceof, jak i w switch. To sprawia, że kod jest krótszy, bezpieczniejszy i bardziej zrozumiały.
2. Składnia record patterns: najprostszy przykład
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);
}
Co tu się dzieje?
- obj instanceof Point(int x, int y) — sprawdzamy typ i od razu rozpakowujemy komponenty do zmiennych x i y.
- Zmienne x i y są dostępne tylko wewnątrz bloku if.
Wynik:
x=10, y=20
Uwaga: jeśli obj nie jest Point lub jest null, blok if nie zostanie wykonany, a zmienne nie zostaną zadeklarowane.
3. Record patterns w switch: zwięzłość i bezpieczeństwo
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!");
}
- W case Point(int x, int y) od razu mamy dostęp do x i y.
- W case Circle(Point center, int r) dostępne są center i r.
Wynik:
Point: x=3, y=4
Z sealed-hierarchiami switch staje się wyczerpujący: jeśli zapomnisz obsłużyć wariant, kompilator ostrzeże.
4. Zagnieżdżone record patterns: rozpakowywanie wewnątrz rozpakowywania
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 + ")");
}
Wynik:
Line from (1,2) to (3,4)
Pracujesz z drzewami, grafami, złożonymi strukturami? Zagnieżdżone wzorce to twoi sprzymierzeńcy.
5. Record patterns z wyrażeniami guard (when)
Object obj = new Point(0, 100);
if (obj instanceof Point(int x, int y) && x == 0) {
System.out.println("Punkt leży na osi Y: y=" + y);
}
W switch robi się to za pomocą when:
switch (obj) {
case Point(int x, int y) when x == 0 -> System.out.println("Na osi Y: y=" + y);
case Point(int x, int y) when y == 0 -> System.out.println("Na osi X: x=" + x);
case Point(int x, int y) -> System.out.println("Zwykły punkt");
default -> System.out.println("To nie punkt");
}
6. Zastosowanie record patterns w realnej aplikacji
Weźmy praktyczny przykład — figury geometryczne.
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");
}
}
Przykład wywołania:
printShapeInfo(new Rectangle(new Point(5, 10), 20, 30));
Wynik:
Rectangle: topLeft=(5, 10), size=20x30
7. Ograniczenia i niuanse użycia record patterns
Tylko dla klas record. Record patterns działają tylko z obiektami, które rzeczywiście są record-klasami.
class NotARecord {
int a, b;
NotARecord(int a, int b) { this.a = a; this.b = b; }
}
// Błąd! To nie jest record
// if (obj instanceof NotARecord(int a, int b)) { ... }
Zgodność komponentów co do liczby i typu. Wzorzec musi odpowiadać komponentom klasy record.
record Pair(int a, String b) {}
// Błąd: typy się nie zgadzają
// if (obj instanceof Pair(String a, int b)) { ... }
Zmienne dostępne tylko wewnątrz bloku. Nazwy zadeklarowane w pattern są widoczne w ciele konkretnego if lub case, gdzie dopasowanie zakończyło się powodzeniem.
8. Zagnieżdżone wzorce: przykład z drzewem
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);
};
}
Przykład użycia:
Expr e = new PlusExpr(new NumberExpr(7), new MinusExpr(new NumberExpr(10), new NumberExpr(3)));
System.out.println(eval(e)); // 7 + (10 - 3) = 14
Tutaj od razu rozpakowujemy NumberExpr(int value), PlusExpr(Expr left, Expr right) i MinusExpr(Expr left, Expr right) — kod staje się zwięzły i wyrazisty.
9. Tabela: porównanie dopasowywania wzorców z i bez record patterns
| Sposób | Ręczne rzutowanie typu | Dopasowywanie wzorców z record patterns |
|---|---|---|
| Sprawdzenie typu | |
|
| Rzutowanie typu | |
Nie trzeba |
| Dostęp do pól | |
|
| Zagnieżdżone rozpakowywanie | Dużo ręcznego kodu | Zagnieżdżone wzorce |
| Użycie w switch | Niewygodne/niemożliwe | Łatwo i zwięźle |
10. Typowe błędy przy użyciu record patterns
Błąd nr 1: próba zastosowania record pattern do klasy, która nie jest recordem. Jeśli napisaliście zwykłą klasę, a potem próbujecie „rozpakować” ją jak record, kompilator na to nie pozwoli. Record patterns działają tylko z prawdziwymi record-klasami.
Błąd nr 2: niezgodna liczba lub typ komponentów. Wzorzec musi w pełni odpowiadać sygnaturze komponentów record. Na przykład dla record Pair(int a, String b) nie można pisać Pair(int x, int y).
Błąd nr 3: próba użycia zmiennych wzorca poza blokiem. Nazwy zadeklarowane w wzorcu są dostępne tylko wewnątrz odpowiedniego if/case. Poza blokiem takie zmienne nie istnieją.
Błąd nr 4: używanie record patterns na starych wersjach JDK. Wsparcie pojawiło się w Javie 21. Na Javie 17 i starszych dostaniesz błąd składni. Sprawdź wersję JDK i wsparcie w IDE.
Błąd nr 5: zbyt głęboko zagnieżdżone wzorce. Możliwości zagnieżdżania są potężne, ale nie komplikuj bez potrzeby: zbyt głęboka struktura pogarsza czytelność i zwiększa ryzyko błędów.
GO TO FULL VERSION