1. Klasyczny switch: ograniczenia przeszłości
Przypomnijmy, jaki był switch w Javie przed pojawieniem się dopasowywania wzorców. Był, szczerze mówiąc, dość konserwatywny. Działał tylko na prymitywach (int, char, a nieco później enum, String), a jeśli chcieliście obsłużyć różne typy obiektów — trzeba było pisać długie łańcuchy if-else.
Object obj = ...;
if (obj instanceof String) {
String s = (String) obj;
System.out.println("Napis: " + s);
} else if (obj instanceof Integer) {
Integer i = (Integer) obj;
System.out.println("Liczba: " + i);
} else if (obj == null) {
System.out.println("null!");
} else {
System.out.println("Coś innego");
}
Wygodne? Nieszczególnie. Kod jest rozwlekły, łatwo o błąd, a jeśli dojdzie nowy typ — znów trzeba przepisywać łańcuch if-else.
2. Dopasowywanie wzorców w switch: rewolucja w Javie 17/21+
Java 17 (preview), a finalnie Java 21+, wprowadziła nową wersję switch, która wspiera dopasowywanie wzorców (pattern matching). Teraz switch może działać nie tylko z prymitywami, lecz także z dowolnymi obiektami i typami, nawet z null, oraz wspiera dodatkowe warunki (wyrażenia guard).
Nowa składnia: zwięzłość i moc
Przykład prostego dopasowywania wzorców w switch:
Object obj = ...;
switch (obj) {
case String s -> System.out.println("Napis: " + s);
case Integer i -> System.out.println("Liczba: " + i);
case null -> System.out.println("null!");
default -> System.out.println("Coś innego");
}
Co tu się dzieje?
- Jeśli obj jest napisem, zmienna s automatycznie ma typ String.
- Jeśli obj jest liczbą całkowitą, zmienna i staje się typem Integer.
- Jeśli obj to null, zadziała specjalna gałąź.
- Wszystko pozostałe — default.
Dlaczego to jest wygodne?
- Bez ręcznego rzutowania typów: Java sama dba o bezpieczeństwo typów.
- Mniej kodu: Bez zbędnych nawiasów, rzutowań i duplikacji.
- Bezpieczniej: Kompilator sprawdza, czy nie zapomniano obsłużyć możliwych wariantów (zwłaszcza przy sealed-klasach, o których powiemy później).
- Łatwo dodawać nowe typy: Po prostu dodaj kolejną gałąź case.
3. Dopasowywanie wzorców w switch: podstawowe przykłady
Przykład 1: Obsługa różnych typów
Object obj = 42;
switch (obj) {
case String s -> System.out.println("Napis: " + s);
case Integer i -> System.out.println("Liczba: " + i);
case null -> System.out.println("To jest null");
default -> System.out.println("Nieznany typ");
}
Wynik:
Liczba: 42
Przykład 2: Użycie wyrażeń guard (when)
Czasem samo poznanie typu to za mało — chcemy dodać dodatkowy warunek.
Object obj = "Hello, world!";
switch (obj) {
case String s when s.length() > 10 -> System.out.println("Długi napis: " + s);
case String s -> System.out.println("Krótki napis: " + s);
case Integer i when i > 0 -> System.out.println("Liczba dodatnia: " + i);
case Integer i -> System.out.println("Liczba niedodatnia: " + i);
default -> System.out.println("Coś innego");
}
Wynik:
Długi napis: Hello, world!
Zwróć uwagę:
- when to wyrażenie guard, dodatkowa weryfikacja dla case.
- Jeśli warunek nie jest spełniony, sprawdzane są kolejne case.
Przykład 3: Obsługa null
Object obj = null;
switch (obj) {
case null -> System.out.println("To jest null!");
default -> System.out.println("Nie null");
}
Wynik:
To jest null!
4. Dopasowywanie wzorców w switch z klasami i dziedziczeniem
Dopasowywanie wzorców w switch szczególnie dobrze działa z hierarchiami klas.
sealed interface Shape permits Circle, Rectangle, Square {}
final class Circle implements Shape {
final double radius;
Circle(double radius) { this.radius = radius; }
}
final class Rectangle implements Shape {
final double width, height;
Rectangle(double width, double height) { this.width = width; this.height = height; }
}
final class Square implements Shape {
final double side;
Square(double side) { this.side = side; }
}
Shape shape = new Rectangle(2, 3);
String description = switch (shape) {
case Circle c -> "Okrąg o promieniu " + c.radius;
case Rectangle r -> "Prostokąt " + r.width + "x" + r.height;
case Square s -> "Kwadrat o boku " + s.side;
};
System.out.println(description);
Wynik:
Prostokąt 2.0x3.0
Ważne: Jeśli używasz sealed-klas i obsłużysz wszystkie ich warianty, gałąź default nie jest wymagana — kompilator rozumie, że innych wariantów być nie może!
5. Użycie switch z klasami record i wzorcami zagnieżdżonymi
Dopasowywanie wzorców w switch wspiera pracę z klasami record i wzorcami zagnieżdżonymi (począwszy od Java 21+).
record Point(int x, int y) {}
record Line(Point start, Point end) {}
Object obj = new Line(new Point(1, 2), new Point(3, 4));
String result = switch (obj) {
case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
"Linia od (" + x1 + "," + y1 + ") do (" + x2 + "," + y2 + ")";
case Point(int x, int y) -> "Punkt (" + x + "," + y + ")";
default -> "Nieznany obiekt";
};
System.out.println(result);
Wynik:
Linia od (1,2) do (3,4)
6. Specyfika i ograniczenia dopasowywania wzorców w switch
Zmienne są dostępne tylko wewnątrz case
Zmienna zadeklarowana w wzorcu (na przykład String s) istnieje tylko wewnątrz odpowiadającego jej bloku case. Poza nim jest niedostępna.
switch (obj) {
case String s -> System.out.println(s); // tutaj s jest widoczne
// System.out.println(s); // tu błąd kompilacji!
}
Obsługa null
- W starym switch na obiektach próba przekazania null prowadziła do NullPointerException.
- W nowym dopasowywaniu wzorców switch można jawnie obsłużyć null przez case null.
Wyrażenia guard (when)
Dodatkowe warunki (when) są dostępne tylko w switch w stylu wyrażenia (składnia strzałkowa ->).
case String s when s.isEmpty() -> System.out.println("Pusty napis");
Wymagania dla JDK i IDE
- Dopasowywanie wzorców w switch to funkcja Java 17 (preview) i Java 21+ (stabilnie).
- Do działania potrzebny jest JDK 21 i IDE wspierające nowe możliwości (na przykład IntelliJ IDEA 2023+).
7. Zastosowanie dopasowywania wzorców w switch w rzeczywistej aplikacji
Przyjrzyjmy się aplikacji edukacyjnej — prostemu trackerowi zadań. Załóżmy, że masz abstrakcyjny typ zadania i różne jego rodzaje:
sealed interface Task permits SimpleTask, DeadlineTask, MeetingTask {}
final class SimpleTask implements Task {
final String description;
SimpleTask(String description) { this.description = description; }
}
final class DeadlineTask implements Task {
final String description;
final java.time.LocalDate deadline;
DeadlineTask(String description, java.time.LocalDate deadline) {
this.description = description;
this.deadline = deadline;
}
}
final class MeetingTask implements Task {
final String topic;
final java.time.LocalTime time;
MeetingTask(String topic, java.time.LocalTime time) {
this.topic = topic;
this.time = time;
}
}
Cała logika w jednym, zwięzłym switch:
Task task = new DeadlineTask("Oddać projekt", java.time.LocalDate.of(2024, 7, 1));
String info = switch (task) {
case SimpleTask st -> "Zwykłe zadanie: " + st.description;
case DeadlineTask dt -> "Zadanie z terminem: " + dt.description + ", do " + dt.deadline;
case MeetingTask mt -> "Spotkanie: " + mt.topic + " o " + mt.time;
};
System.out.println(info);
Wynik:
Zadanie z terminem: Oddać projekt, do 2024-07-01
8. Typowe błędy przy używaniu dopasowywania wzorców w switch
Błąd nr 1: zapomniano obsłużyć null. Jeśli nie dodasz case null, a obj może być null — otrzymasz MatchException (lub NullPointerException w starych switch). Zawsze jawnie obsługuj null, jeśli to możliwe.
Błąd nr 2: nie uwzględniono wszystkich potomków klasy sealed. Jeśli używasz sealed-klasy i nie obsłużysz wszystkich wariantów w switch, kompilator zgłosi błąd lub wymusi gałąź default. Nie zapominaj dodawać nowych case przy rozszerzaniu hierarchii.
Błąd nr 3: zmienna wzorca jest niedostępna poza blokiem case. Zmienna zadeklarowana w wzorcu istnieje tylko wewnątrz bloku strzałkowego. Próba użycia jej poza tym blokiem spowoduje błąd kompilacji.
Błąd nr 4: używanie nowych funkcji na starej JDK lub w starej IDE. Dopasowywanie wzorców w switch wymaga Java 21+ i odpowiedniego wsparcia w IDE. Jeśli użyjesz starej wersji — pojawi się błąd kompilacji lub niepoprawne podpowiedzi.
Błąd nr 5: zamieszanie z wyrażeniami guard. Próbujesz używać when w starym blokowym switch? Nie zadziała. Guardy są dostępne tylko dla składni strzałkowej ->.
Błąd nr 6: zapomniano o gałęzi default przy pracy z otwartymi hierarchiami. Jeśli Twój switch działa nie tylko z sealed-klasami, koniecznie dodaj default — inaczej nie wszystkie warianty zostaną obsłużone.
GO TO FULL VERSION