Java 12 - wyrażenie switch (preview feature)
Poniższy post stanowi luźny przegląd dokumentaji JEP-361 Switch expressions, wprowadzonych ostatecznie w javie 14.
Nowości w języku
Status “preview feature”
Java 12 wprowadza wyrażenie switch jako ulepszoną alternatywę dla istniejącej od zarania dziejów instrukcji switch. Jest to nowość dostępna jako preview feature, co oznacza, że jej użycie w kodzie kompilowanym jako --source 12 wymaga podania opcji --enable-preview zarówno do kompilatora jak i launchera.
Na przykład, aby skompilować Simple.java pod jdk 12, należy użyć polecenia:
$ javac --enable-preview --release 12 Simple.javaKompilacja kodu wykorzystującego elementy języka o statusie prevew skutkuje wypisaniem następującego komunikatu:
Note: Simple.java uses preview language features.
Note: Recompile with -Xlint:preview for detailsInstrukcja switch
Istniejący w javie switch jest instrukcją, nie wyrażeniem. Jest to instrukcja niskopoziomowa, zaimplementowana dla zachowania “zgodności mentalnej” programistów używających wcześniej C lub C++. Używana również w wysokopoziomowych zastosowaniach w Javie od dawna sprawia kilka problemów. Są to:
- Nieintuicyjne i podatne na błędy zachowanie fallthrough, czyli wykonanie kodu wszystkich następujących po bieżącym
caseprzypadków, jeśli bieżącycasenie kończy się instrukcjąbreak. Większość przypadków użyciacasenie korzysta z właściwości fallthrough, co wymusza pisaniebreakw każdymcase.
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
System.out.println(6);
break;
case TUESDAY:
System.out.println(7);
break;
case THURSDAY:
case SATURDAY:
System.out.println(8);
break;
case WEDNESDAY:
System.out.println(9);
break;
}- Wiele instrukcji
breakw mocno rozgałęzionymcasezmniejsza czytelność kodu. - Zakresem leksykalnym zmiennej zadeklarowanej w pewnej gałęzi
casejest cały kod od jej deklaracji do końcaswitch, co sprawia, że zmienne tymczasowe - reprezentujące ten sam koncept w każdej z gałęzi - muszą się w każdej gałęzi inaczej nazywać. Bardziej odpowiednie byłoby zawężenie zakresu do bieżącej gałęzi.
switch (day) {
case MONDAY:
case TUESDAY:
int temp = ...
// zakres leksykalny 'temp' aż do końcowego nawiasu }
break;
case WEDNESDAY:
case THURSDAY:
int temp2 = ...
// nie można nazwać tej zmiennej 'temp'
break;
default:
int temp3 = ...
// nie można nazwać tej zmiennej 'temp'
}- Istniejące użycia
switchczęsto symulują działanie wyrażenia przez przypisywanie w kolejnych gałęziach wartości do tej samej, zadeklarowanej przed instrukcjąswitch, zmiennej. Znów - zmniejsza to czytelność kodu i wydaje się nadmiarowe.
int numLetters;
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
numLetters = 5;
break;
case TUESDAY:
numLetters = 6;
break;
case THURSDAY:
case SATURDAY:
numLetters = 7;
break;
case WEDNESDAY:
numLetters = 8;
break;
default:
throw new IllegalStateException("Wat: " + day);
}Wyrażenie switch (preview)
Wszystkie powyższe problemy rozwiązuje wyrażenie switch.
- wprowadzona jest nowa forma labela dla
switch, tzw. arrow label, w postacicase L ->, przy czym możliwe jest podanie wielu labelek oddzielonych przecinkiem w jednej gałęzicase. Powyższy przykład z dniami tygodnia będzie miał dużo czytelniejszą postać:
switch (day) {
case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
case TUESDAY -> System.out.println(7);
case THURSDAY, SATURDAY -> System.out.println(8);
case WEDNESDAY -> System.out.println(9);
}- domyślnie nie występuje fallthrough, wykonywany jest jedynie kod po strzałce (
->) switchjako wyrażenie zwraca wartość, którą można przypisać do zmiennej. Można więc napisać znacznie bardziej spójny kod:
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
}
System.out.println(numLetters);- jeśli programista pragnie użyć semantyki
falthroughw wyrażeniuswitch, może to zrobić, używając tradycyjnej formy labela, czyli tzw. colon label, w postacicase L:
Instrukcja yield
W przypadku, gdy w danej gałęzi wykonywanych jest wiele instrukcji, należy je ogarnąć wąsatymi nawiasami ({ ... }), a wartość należy zwrócić za pomocą instrukcji yield w postaci yield value;. Instrukcja ta może być używana wyłącznie w wyrażeniu switch ale nie w instrukcji switch, w której z gałęzi można “wyjść” albo przez fallthrough albo przy użyciu instrukcji break.
int j = switch (day) {
case MONDAY -> 0;
case TUESDAY -> 1;
default -> {
int k = day.toString().length();
int result = f(k);
yield result;
}
};Wyczerpowalność (exhaustiveness)
Jeśli istnieje polskie tłumaczenie słowa exhaustiveness, to ja go chyba nie znam. O co chodzi z wyczerpywalnością? Otóż kompilator gwarantuje, że dla dowolnej wartości w wyrażeniu switch zawsze istnieje gałąż case. (Warto przypomnieć, że instrukcja switch takich gwarancji nie daje). Jak to jest możliwe? Oczywiście, gwarancja polega na tym, że kompilator rzuci wyjątkiem, gdy wszystkie przypadki nie są pokryte.
W instrukcji switch, jeśli wybór gałęzi dotyczy typu Enum, kompilator gwarantuje, że pokryta jest każda stała (i że definicja klasy Enum nie zmieniła się między kompilacją a uruchomieniem) w taki sposób, że kompilator generuje automatycznie przypadek default. Jeśli kod zostanie przekompilowany, a ktoś w międzyczasie rozszerzył definicję klasy Enum, kompilator wykryje brak pokrycia (a więc fakt, że nie wszyskie przypadki są wyczerpane czy użyte) i elegancko wyrzuci błąd. Oczywiście, możemy “zadowolić” kompilator pisząc ręcznie gałąź default, i wówczas już zachowanie kodu w przypadku “niepokrytych” wartości zależy od programisty.
Wymóg zwrócenia wartości
Wyrażenie switch musi zwracać w każdej gałęzi wartość (kończyć się normalnie z pewną wartością) lub rzucić wyjątek (kończyć się gwałtownie z wyjątkiem).
int i = switch (day) {
case MONDAY -> {
System.out.println("Monday");
// Błąd - blok nie zawiera instrukcji yield
}
default -> 1;
};
i = switch (day) {
case MONDAY, TUESDAY, WEDNESDAY:
yield 0;
default:
System.out.println("Second half of the week");
// Błąd: gałąź nie zawiera instrukcji yield
};Oznacza to, że nie można “uciec” z instrukcji switch w żaden inny sposób, na przykład przez użycie instrukcji continue:
z:
for (int i = 0; i < MAX_VALUE; ++i) {
int k = switch (e) {
case 0:
yield 1;
case 1:
yield 2;
default:
continue z;
// Błąd: niepoprawny skok na zewnątrz wyrażenia switch
};
...
} This text is part of the series java...
- 2025-21-03 - Java 24 Overview
- 2024-01-11 - Java 23 w praktyce
- 2021-27-11 - JDK 18 proposals: Java 18
- 2021-21-09 - Java 17 - RandomGenerator i spółka
- 2021-15-09 - Java 17 - co nowego?
- 2021-04-03 - Java 15 - czym są sealed classes?
- 2021-26-02 - Java 13 i 14: Bloki tekstowe i rekordy
- 2021-24-02 - Java 12 - wyrażenie switch (preview feature)
- 2021-23-02 - Java 11 - HTTP Client i uruchamianie jednoplikowych programów
- 2021-18-02 - Java 9 - co to jest JShell i dlaczego warto używać REPL-a w Javie
- 2021-12-02 - Java 10 - var, nowe metody w Optional, kolekcje "unmodifiable"
- 2021-11-02 - Java 9 - nowości w bibliotece
- 2021-10-02 - Java 9: czy mogę stworzyć z mojej aplikacji binarkę?
- 2021-10-02 - Java 9: praktyczny przykład - trzy moduły
- 2021-08-02 - Java 8: praktyczny przykład - przewidywanie kolejnej daty w serii