Spis treści

Java 12 - wyrażenie switch (preview feature)

Java 12 - ilustracja - jogin

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:

1
$ javac --enable-preview --release 12 Simple.java

Kompilacja kodu wykorzystującego elementy języka o statusie prevew skutkuje wypisaniem następującego komunikatu:

1
2
Note: Simple.java uses preview language features.
Note: Recompile with -Xlint:preview for details

Instrukcja 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 case przypadków, jeśli bieżący case nie kończy się instrukcją break. Większość przypadków użycia case nie korzysta z właściwości fallthrough, co wymusza pisanie break w każdym case.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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 break w mocno rozgałęzionym case zmniejsza czytelność kodu.
  • Zakresem leksykalnym zmiennej zadeklarowanej w pewnej gałęzi case jest cały kod od jej deklaracji do końca switch, 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.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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 switch czę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.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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 postaci case L ->, przy czym możliwe jest podanie wielu labelek oddzielonych przecinkiem w jednej gałęzi case. Powyższy przykład z dniami tygodnia będzie miał dużo czytelniejszą postać:
1
2
3
4
5
6
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 (->)
  • switch jako wyrażenie zwraca wartość, którą można przypisać do zmiennej. Można więc napisać znacznie bardziej spójny kod:
1
2
3
4
5
6
7
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 falthrough w wyrażeniu switch, może to zrobić, używając tradycyjnej formy labela, czyli tzw. colon label, w postaci case 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.

1
2
3
4
5
6
7
8
9
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).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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
  };
  ...
  }